Fixing Google OAuth Token Expiry: Moving from Testing to Production Consent Screen

We've been battling a recurring OAuth authentication issue where Google refresh tokens expire after 7 days, forcing manual re-authentication cycles across our calendar sync infrastructure. After weeks of band-aid token refreshes, the root cause is finally clear: our Google Cloud project's OAuth consent screen is stuck in Testing mode, which imposes hard expiry limits on refresh tokens. This post documents the permanent fix.

The Problem: Testing Mode Token Expiry

Our booking automation system relies on three main OAuth flows:

  • Google Calendar API — syncing JADA events to third-party platforms (Boatsetter, Sailo, Viator)
  • Google Sheets API — reading crew assignments and dashboard state from Apps Script
  • Gmail API — sending booking confirmations and crew notifications

Each of these integrations authenticates via a service account token stored in `/Users/cb/Documents/repos/tools/` and synced to our AWS Lambda environment. The problem: when a Google Cloud project's OAuth consent screen is in Testing mode, refresh tokens are valid for only 7 days. After 7 days of inactivity, the token becomes invalid, causing calendar writes to fail silently and triggering a manual re-auth cycle via our `reauth_jada_all.py` script.

We've re-authed maybe five times in the past two months. Each time, someone (usually me) has to:

  1. Run the local reauth script to get a new token from Google
  2. Upload the new token to Lambda via `tools/reauth_jada_all.py`
  3. SSH into the Lightsail instance and manually sync tokens there
  4. Restart the calendar Lambda or wait for the next invocation

This is operationally unsustainable. The permanent fix is moving our OAuth consent screen to Production mode, which grants refresh tokens with indefinite validity (they only expire after 6 months of inactivity, not 7 days).

Why This Wasn't Fixed Before

Transitioning to Production mode requires publishing an OAuth app, which means:

  • Filling out an OAuth consent screen with app name, privacy policy, and support contact
  • Adding verified scopes for Calendar, Sheets, and Gmail APIs
  • Potentially submitting for Google's security review (depending on scopes requested)
  • Maintaining a published app name and privacy policy going forward

Previously, the path of least resistance was just re-authing every few weeks. But with May bookings ramping up and crew dispatch becoming more complex, we can't afford manual intervention on this cadence.

Technical Implementation

Google Cloud Console Changes

The fix is manual via the Google Cloud Console (no API currently exposes the OAuth consent screen UI). Here's the exact path:

Google Cloud Console 
  → APIs & Services 
  → OAuth consent screen 
  → Change from "Testing" to "Production"

Once in Production mode, you'll be prompted to fill out:

  • App name: "JADA Sailing Calendar Sync"
  • User support email: jadasailing@gmail.com (verified in Google Workspace)
  • App logo: Optional, but recommended for clarity
  • Scopes:
    • https://www.googleapis.com/auth/calendar (Calendar read/write)
    • https://www.googleapis.com/auth/spreadsheets (Sheets read)
    • https://www.googleapis.com/auth/gmail.send (Gmail send only)
  • Privacy policy URL: Link to your privacy policy (can be a simple page on your domain)
  • Terms of service URL: Optional but recommended

Since we're not requesting sensitive scopes (we're not asking for user contacts, location, etc.), Google's review process is typically fast — 24-48 hours.

Code Changes Required

The good news: no code changes are required. The refresh token behavior is entirely governed by Google's consent screen mode. Our existing token management in `tools/reauth_jada_all.py` will continue to work; tokens will just stay valid indefinitely instead of expiring after 7 days.

However, we should update our token rotation strategy slightly:

  • Remove the 7-day forced re-auth check from our monitoring (if one exists)
  • Add a 6-month token refresh reminder for Lambda environment rotation
  • Document in the reauth script that this is now preventative, not emergency

Lambda Environment Updates

Once the new token is generated post-Production mode transition, it's deployed to Lambda via the existing workflow:

# Local token generation
python3 tools/reauth_jada_all.py

# Upload to Lambda
aws lambda update-function-configuration \
  --function-name calendar-sync-jada \
  --environment Variables={GOOGLE_TOKEN=<new-token>}

The Lambda function itself (`calendar-sync-jada` in us-east-1) reads the token from environment variables at runtime and uses it for all Google API calls in `CalendarSync.gs`.

Why We Use Testing Mode in the First Place

Testing mode was appropriate during initial development because:

  • Quick iteration without review delays
  • Only our own accounts needed access (no external users)
  • Lower security overhead

However, once we moved from development to production booking operations (especially with real customer data flowing through Boatsetter → JADA calendar), the 7-day token expiry became operationally untenable. Production mode is the correct long-term home for this infrastructure.

Related Issues This Fixes

Moving to Production mode also unblocks:

  • Boatsetter auto-sync: Calendar API writes currently fail silently when tokens expire. This fix ensures writes go through consistently.
  • Sailo sync: Same as Boatsetter — both platforms pull our iCal feed, but we push updates via our own Calendar API calls.
  • Dashboard notification emails: Gmail API calls for crew notifications will no longer fail mid-month.

Timeline and Next Steps

  1. Today: Transition OAuth consent screen to Production in Google Cloud Console
  2. 24-48 hours: Wait for Google's review (usually fast for non-sensitive scopes)
  3. Once approved: Run `tools/reauth_jada_all.py` to generate a fresh token
  4. Same day: Deploy token to Lambda and Lightsail instances
  5. Verification: Test calendar syncs for Boatsetter