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 & Services → OAuth 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
- 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).
- 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
- 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.
- Deploy New Token: Sync the new token file to Lightsail via the existing SCP pipeline.
- Verify Calendar Sync: Test the Lambda calendar endpoint to confirm reads/writes work without errors.
- 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