Fixing Permanent OAuth Token Expiration: Moving Google Cloud OAuth from Testing to Production Mode

The Problem: 7-Day Token Refresh Cycle in Testing Mode

We've been stuck in a recurring authentication cycle where OAuth refresh tokens expire every 7 days, forcing manual re-authentication. This isn't a code bug or a token rotation issue — it's a Google Cloud platform limitation. When an OAuth application is in Testing mode, Google intentionally caps refresh token lifetime at 7 days as a security measure for development. Every 7 days, our Lambda functions, Apps Script deployments, and CLI tools lose the ability to refresh their access tokens and fail silently or with authentication errors.

The previous "fix" was a band-aid: regenerate tokens and push them to Lambda. But tokens would expire again in a week. The root cause — Testing mode configuration — was never addressed.

Why This Matters for Our Architecture

Our booking automation stack relies on several OAuth-authenticated services:

  • Google Calendar API: Lambda functions in /opt/lambda/calendar_sync query calendar events to sync with Boatsetter, Sailo, and Viator
  • Google Sheets API: Apps Script in the GAS deployment pulls crew/captain data from our operational sheets
  • Google Drive API: Document generation and state file updates depend on uninterrupted token access
  • Gmail API: Booking confirmations, crew notifications, and digest emails are sent via service account tokens

With tokens expiring every 7 days, any of these integrations can fail. The user is forced to manually re-auth, which is operationally unsustainable at booking volume.

Technical Details: Testing Mode vs. Production Mode

Google Cloud's OAuth consent screen has two operational modes:

  • Testing Mode: For development/testing. Tokens refresh every 7 days. Users are added individually. No audit logging. (This is what we're in.)
  • Production Mode: For published applications. Tokens refresh every 6 months (unless revoked). Users can authenticate without being explicitly added. Full audit trail. Requires app verification and privacy policy.

Our project — stored in Google Cloud Console under the JADA Sailing project ID — currently has the OAuth app in Testing mode. Moving to Production requires publishing the app, which involves:

  1. Setting up a privacy policy URL (can be self-hosted or use a template)
  2. Configuring the OAuth brand (app name, logo, support email)
  3. Declaring all requested scopes and their usage
  4. Submitting for verification (if using sensitive scopes like email, calendar, drive)

The Fix: Moving to Production Mode

Here's the exact workflow:

Step 1: Verify Current Consent Screen Status

gcloud auth application-default login
gcloud config set project jada-sailing-prod

# Check which scopes are currently requested
gcloud projects describe jada-sailing-prod --format="value(name)"

The scopes our system requires (from existing token files and Lambda environment):

  • https://www.googleapis.com/auth/calendar — Calendar read/write
  • https://www.googleapis.com/auth/drive — Drive file access
  • https://www.googleapis.com/auth/spreadsheets — Sheets read/write
  • https://www.googleapis.com/auth/gmail.send — Gmail sending only (not inbox access)

These are all non-sensitive from Google's perspective (we're not requesting Drive full access, just file-level), so verification should be straightforward.

Step 2: Configure the OAuth Brand

Via Google Cloud Console (UI-only, no CLI equivalent):

  1. Navigate to APIs & ServicesOAuth consent screen
  2. Set User Type to External (since users are Boatsetter customers + crew)
  3. Fill in required fields:
    • App name: "JADA Sailing Booking Automation"
    • User support email: tech@queenofsandiego.com
    • Developer contact: tech@queenofsandiego.com
  4. Add a privacy policy URL — this can be a static page served from the tech blog or a Google-hosted placeholder
  5. Save as draft

Step 3: Add Scopes in Consent Screen Configuration

In the same consent screen UI:

  1. Click Add or Remove Scopes
  2. Select the four scopes above
  3. Verify the scope descriptions are clear (Google provides defaults)
  4. Save

Step 4: Publish the App

Still in the consent screen UI:

  1. Click the radio button to change from Testing to Production
  2. Review the verification prompt — Google may require manual review for calendar/drive access, or may auto-approve
  3. Submit

Once approved (usually 24-48 hours for non-sensitive scopes), the OAuth app moves to Production mode and all new tokens generated will have 6-month refresh cycles instead of 7-day cycles.

What Happens to Existing Tokens?

Existing tokens (already generated in Testing mode) will retain their 7-day refresh lifecycle — they don't retroactively upgrade. So once Production mode is live, we need to:

  1. Trigger a new OAuth flow for all service accounts and Lambda environments
  2. Regenerate and push tokens to:
    • Lambda environment variables (via CloudFormation or direct update)
    • Apps Script bound scripts (via clasp deployment with new creds)
    • Lightsail server token files
    • Local development environment (~/.config/gcloud/, etc.)

The re-auth is a one-time operation once Production mode is live. After that, tokens will refresh automatically for 6 months without manual intervention.

Infrastructure Changes Required

Once tokens are regenerated:

  • Lambda function (jada-calendar-sync-prod in us-west-2): Update environment variable GOOGLE_OAUTH_TOKEN via CloudFormation stack update or direct AWS Lambda console
  • Apps Script (deployment ID in Google Workspace): Push new token via clasp to update bound script credentials
  • Lightsail instance (jada-automation-prod): Sync new token files via SSH to /var/lib/jada/tokens/
  • SES configuration: If token-based auth is used, update credentials in /etc/jada/ses.conf

Key