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

The Problem: Seven-Day Token Refresh Cycle

For months, the JADA sailing platform has been caught in a frustrating cycle: every seven days, Google OAuth refresh tokens expire, breaking calendar synchronization across multiple booking platforms (Boatsetter, Sailo, Viator). Each time, the solution was manual re-authentication—clicking through Google's consent screen, extracting new tokens, and pushing them to Lambda. This wasn't a token management bug; it was a configuration oversight at the Google Cloud project level.

The root cause: the OAuth consent screen was configured in Testing mode, which intentionally restricts refresh token lifetime to seven days. This is a security feature for development, not a production limitation. To achieve permanent tokens, the application needed to be published to Production mode on the OAuth consent screen.

Technical Architecture: Current Token Flow

Before diving into the fix, understanding the current architecture is essential:

  • Token Source: OAuth tokens are generated via a local Python script (`/Users/cb/Documents/repos/tools/reauth_jada_all.py`) that runs the Google OAuth 2.0 authorization flow offline, storing credentials in local JSON files.
  • Token Distribution: Tokens are synced to an AWS Lightsail instance via SCP, where they're available to Lambda functions.
  • Lambda Consumption: Calendar synchronization Lambda functions (deployed via API Gateway at specific routes) read tokens from Lightsail, then invoke Google Calendar API and Apps Script execution APIs.
  • GAS Integration: Google Apps Script (`CalendarSync.gs`) contains the master calendar sync logic, deployed as a web app with specific scopes including `calendar`, `drive`, and `script execution`.

The seven-day expiration meant this entire chain required manual intervention weekly—undermining the entire automation premise.

Solution: OAuth Consent Screen Production Mode

The fix involves two components:

1. OAuth Consent Screen Configuration (Google Cloud Console)

Navigate to the Google Cloud Console for the JADA project (already authed via `gcloud CLI`). Under APIs & ServicesOAuth Consent Screen, the configuration consists of:

  • User Type: Change from "Internal" (testing) to "External" (production). This unlocks the "Publish to Production" button.
  • App Name: "JADA Sailing Platform"
  • User Support Email: jadasailing@gmail.com
  • Developer Contact: Same email
  • Scopes Required: The application uses:
    • https://www.googleapis.com/auth/calendar (calendar read/write)
    • https://www.googleapis.com/auth/drive (for deployment files)
    • https://www.googleapis.com/auth/script.projects (Apps Script execution)
  • Branding: Add logo URI (can point to S3 CloudFront distribution if one exists for JADA assets)

Critical Decision: While "External" mode typically triggers Google's verification flow (24-48 hours), JADA qualifies for an exception: it's not a public SaaS product. It's a private tool used by a specific organization. The OAuth app only grants access to JADA's own Google Workspace account (jadasailing@gmail.com). This means verification can be requested as an exception during the publishing process.

2. Token Refresh Logic Update (Optional, But Recommended)

While moving to Production mode solves the seven-day issue, adding defensive token refresh logic in Lambda ensures resilience:

// Pseudocode for Lambda token refresh handler
async function ensureValidToken() {
  const tokenMetadata = readFromS3('s3://jada-lambda-tokens/token_metadata.json');
  const tokenAge = Date.now() - tokenMetadata.issued_at;
  
  // If token is >25 days old (safety margin before 30-day standard expiration)
  if (tokenAge > 25 * 24 * 60 * 60 * 1000) {
    const newToken = await refreshTokenOffline(tokenMetadata.refresh_token);
    await pushToLightsail(newToken);
    await notifySlack('Token refreshed proactively');
  }
  return readFromLightsail('active_token.json');
}

This pattern prevents production failures even if OAuth mode ever reverts or tokens approach expiration for any reason.

Implementation Steps

  1. Verify Current OAuth Credentials: Confirm the GAS web app and Lambda are using the same OAuth client ID (check `Client_ID` field in both token files).
  2. Update OAuth Consent Screen:
    • User Type: Change to "External"
    • Complete all required fields (branding, support email, scopes)
    • Click "Publish to Production"
    • Request verification exception if prompted
  3. Re-authenticate (One Final Time): Run `/Users/cb/Documents/repos/tools/reauth_jada_all.py` once with the updated consent screen active. The new token will have indefinite refresh capability.
  4. Deploy New Token: Sync the new token file to Lightsail via the existing SCP pipeline.
  5. Verify Calendar Sync: Test the Lambda calendar endpoint to confirm reads/writes work without errors.
  6. Trigger GAS Setup** (Critical Missing Step): Call `calendarDashboardSetup()` and `calendarSyncSetup()` in the GAS editor to activate the dormant sync triggers tied to Boatsetter, Sailo, and other iCal feeds.

Why This Fixes the Root Cause

Testing mode refresh tokens are intentionally short-lived because Google assumes the app is still under development—credentials rotate frequently, and seven days forces developers to refresh often for security testing. Production mode tokens are valid for 6 months by default, with refresh capability indefinitely. Once in Production mode, the token lifecycle becomes:

  • Initial Auth: One-time user consent grant
  • Refresh Token: Valid indefinitely (can refresh up to 50 times without re-authenticating)
  • Access Token: Short-lived (1 hour), auto-refreshed by the OAuth library
  • Manual Re-auth Required Only If: User explicitly revokes access, or 6 months pass without any API call (security timeout)

This eliminates the seven-day manual intervention cycle entirely.

Related Fixes: Activating Calendar Sync Triggers

Once tokens are permanent, the second blocker becomes apparent: the GAS sync logic was deployed but never activated. The function `calendarDashboardSetup()` (in `BookingAutomation` project, deployed version v98+) must be invoked to:

  • Enable time-based triggers for iCal polling
  • Register Boatsetter, Sailo, and