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

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

We've been stuck in a recurring cycle of OAuth token failures across our calendar sync infrastructure. Every ~7 days, the Google Calendar API refresh token expires, breaking automated syncs between Boatsetter, Sailo, and our internal JADA calendar system. The root cause? Our Google Cloud OAuth application has been stuck in Testing mode, which Google enforces as a security default—refresh tokens expire after 7 days in this state.

Previous "fixes" involved re-authenticating manually and pushing new tokens to Lambda, but this is unsustainable for production systems. The permanent solution requires moving the OAuth app to Production mode, which extends refresh token lifetime to effectively indefinite (until revoked by the user).

Technical Details: Testing vs. Production Mode

Google Cloud OAuth has two consent screen states:

  • Testing Mode: Allows up to 100 test users, refresh tokens expire after 7 days, suitable only for development
  • Production Mode: Unlimited users, indefinite refresh tokens (revoked only on user action or token rotation), requires application verification

Our setup uses OAuth 2.0 for server-to-server authentication via service accounts and user-delegated scopes. The calendar sync pipeline authenticates as:

  • CalendarSync.gs (Google Apps Script) — writes to JADA Internal calendar via OAuth
  • Lambda function in /reauth_jada_all.py — manages refresh tokens and calendar API calls
  • Boatsetter/Sailo iCal feeds — pull availability from JADA calendar via unauthenticated iCal URL

The token lifecycle flow:

User authenticates (Google login)
  ↓
OAuth consent screen displays scopes
  ↓
Authorization code returned to client
  ↓
Exchange code for access token + refresh token
  ↓
Store refresh token in DynamoDB (table: jada-auth-tokens, key: google_calendar_refresh_token)
  ↓
[Testing Mode: Token valid 7 days] OR [Production Mode: Token valid until revoked]
  ↓
Lambda uses refresh token to request new access tokens on demand

Infrastructure: Google Cloud Console Configuration

The OAuth setup spans two GCP resources:

  • Project ID: jada-sailing-prod
  • OAuth 2.0 Client ID: Web application type (for Apps Script and Lightsail Lambda environment)
  • Consent Screen Configuration: Located in Google Cloud Console → APIs & Services → OAuth consent screen

The consent screen requires two sets of information for Production mode:

  1. App Registration Details:
    • App name: "JADA Sailing Calendar Sync"
    • User support email: jadasailing@gmail.com
    • Developer contact: Same email
    • Authorized domains: Must match your OAuth redirect URIs (e.g., script.google.com for Apps Script, Lightsail instance FQDN for Lambda)
  2. Scopes Declaration:
    • https://www.googleapis.com/auth/calendar — Read/write calendar events
    • https://www.googleapis.com/auth/gmail.send — Send confirmation emails (optional, not used in calendar sync)

Crucially, you do not need Google verification for moving to Production mode in this case—verification is only required if your app accesses highly sensitive scopes (Gmail full access, Drive, etc.). Calendar scope allows automatic approval.

Deployment Steps: Moving to Production

The move involves no code changes—only Console configuration:

  1. Navigate to Google Cloud ConsoleAPIs & ServicesOAuth consent screen
  2. Verify all app registration fields are complete (app name, support email, domains)
  3. Confirm scopes are minimal and necessary
  4. Click "Publish App" (or "Move to Production" depending on console version)
  5. Consent screen status changes from "Testing" to "In Production"
  6. Re-authenticate via CalendarSync.gs or Lambda to generate new token (optional but recommended for clean slate)
  7. Store new refresh token in DynamoDB jada-auth-tokens table
  8. Redeploy Lambda function to pick up new token from environment variable or DynamoDB

Once in Production mode, the refresh token will not expire after 7 days. It remains valid indefinitely until:

  • User manually revokes access in their Google Account settings
  • Token is rotated (generating a new refresh token)
  • Google detects compromised credentials and revokes automatically

Code Context: Where Tokens Are Used

Token management is centralized in two locations:

  • /Users/cb/Documents/repos/tools/reauth_jada_all.py — Python script that handles OAuth flow and token refresh
    • Reads refresh token from DynamoDB
    • Calls Google OAuth token endpoint to exchange for access token
    • Stores new access token in Lambda environment or memory
  • Lambda Environment Variables — stores GOOGLE_CALENDAR_REFRESH_TOKEN and related OAuth credentials
  • DynamoDB Table: jada-auth-tokens — persistent token storage with TTL metadata for monitoring expiration

The Lambda calendar endpoint (API Gateway route: /calendar/sync) invokes the Python function, which:

1. Checks if current access token is expired (compares timestamp vs. current time)
2. If expired, calls Google token refresh endpoint using stored refresh token
3. Updates local token cache (environment variable or parameter store)
4. Makes Google Calendar API call using fresh access token
5. Logs all operations to CloudWatch for audit trail

Why This Matters: Impact on Calendar Sync Systems

Once deployed to Production mode, the following systems will stabilize:

  • CalendarSync.gs — Apps Script trigger runs every 30 minutes, pulls iCal feeds from Boatsetter/Sailo, writes to JADA Internal calendar. Currently fails after 7 days; will run indefinitely.
  • Boatsetter/Sailo Sync — Both platforms pull availability via unauthenticated iCal feed from JADA Internal. Once JADA calendar is being updated reliably, availability blocks propagate automatically.
  • Lambda Calendar Endpoint — Direct API access for programmatic calendar updates (e.g., crew dispatch system). No more manual token refresh cycles.

Monitoring & Next Steps

After deployment: