Fixing Google OAuth Token Expiration: Moving from Testing to Production Mode
After weeks of fighting refresh token expiration every 7 days, we finally identified and resolved the root cause: our Google Cloud OAuth application was stuck in Testing mode, which artificially caps token lifetime. This post covers the technical investigation, the infrastructure changes needed, and why this matters for automated calendar sync across multiple booking platforms.
The Problem: Seven-Day Token Rot
Our calendar synchronization system — which pushes JADA Sailing events to Boatsetter, Sailo, Viator, and GetMyBoat — relies on Google OAuth tokens to write to Google Calendar. Every 7 days, the refresh token would expire, requiring manual re-authentication via reauth_jada_all.py. This was masking a deeper architectural issue.
The symptoms were clear in our Lambda logs and local auth checks:
- Token files at
/home/cb/.local/share/jada/tokens/(Lightsail) and/Users/cb/.local/share/jada/tokens/(local dev) would become stale - Calendar writes would fail silently; the sync trigger in
CalendarSync.gswould not execute - Each fix required running
gcloud auth application-default loginor invoking the OAuth flow manually - Token sync to Lambda environment variables lagged behind actual token expiration
The real issue? Google Cloud Console → APIs & Services → OAuth consent screen was set to Testing. In Testing mode, Google intentionally limits refresh token lifetime to 7 days as a security guardrail for development applications.
Technical Investigation: Finding the Root Cause
We verified the problem by checking multiple layers:
1. Token inspection:
gcloud auth list
gcloud config list
ls -la /home/cb/.local/share/jada/tokens/
All credentials showed valid scopes, but the OAuth app's user_type was always TESTING.
2. Lambda environment verification:
The /tmp/scc-lambda/lambda_function.py reads tokens from environment variables injected during deployment. We verified the tokens were being passed correctly but had stale refresh tokens in the Lambda runtime environment.
3. GAS deployment status:
In /Users/cb/Documents/repos/tools/, our Google Apps Script projects had multiple issues:
CalendarSync.gscontained the sync logic but the trigger was never activatedcalendarDashboardSetup()andcalendarSyncSetup()functions existed but were never called- The deployed web app (found via
clasp deployments) was at v97, missing the setup execution
Infrastructure: Google Cloud OAuth Configuration
The fix requires moving the OAuth application from Testing to Production mode. Here's the exact workflow:
Step 1: Access Google Cloud Console
Navigate to the JADA project (this is UI-only; no CLI equivalent exists for OAuth consent screen configuration):
gcloud config set project jada-calendar-sync-prod
gcloud console open
Then navigate to: APIs & Services → OAuth consent screen
Step 2: Update OAuth consent screen fields
- User Type: Change from "Testing" to "Production"
- App name: "JADA Sailing Calendar Sync" (descriptive, public-facing)
- User support email: Use the OAuth app's service account email (not a personal email)
- Developer contact: Technical point of contact for OAuth issues
- App privacy policy URL: Point to
https://tech.sailjada.com/privacy(or equivalent) - App terms of service URL: Point to
https://tech.sailjada.com/terms
Step 3: Verify OAuth scopes
Ensure these scopes are declared and approved:
https://www.googleapis.com/auth/calendar(read/write calendar events)https://www.googleapis.com/auth/gmail.readonly(read confirmation emails)https://www.googleapis.com/auth/drive.readonly(if pulling event docs)
These scopes must be in the scopes array in our OAuth credentials JSON and declared in appsscript.json for GAS projects.
Step 4: Re-authorize and regenerate tokens
Once the app moves to Production, existing Testing-mode tokens will not automatically upgrade. You must re-run the auth flow:
gcloud auth application-default login
python3 /Users/cb/Documents/repos/tools/reauth_jada_all.py
This script now uses the gcloud CLI to generate fresh tokens with Production-mode lifetime (typically 6 months for offline access).
Step 5: Sync tokens to Lambda and Lightsail
After new tokens are generated, deploy them to all environments:
# Copy to Lightsail via SSH
scp -i ~/.ssh/lightsail-jada /Users/cb/.local/share/jada/tokens/* \
ubuntu@jada-prod-server:/home/cb/.local/share/jada/tokens/
# Update Lambda environment variables via AWS CLI
aws lambda update-function-configuration \
--function-name calendar-sync-production \
--environment Variables={JADA_CALENDAR_TOKEN=...}
GAS Setup Activation
With OAuth fixed, we also needed to activate the calendar sync triggers. The GAS project at clasp push location had deployment version 97, but calendarDashboardSetup() was never invoked.
We updated the deployment to v98 with an explicit setup action:
clasp deploy --description "Activate calendar sync setup"
# Then manually invoke via Google Apps Script editor or scripts.run API:
gcloud run call scripts.run \
--scriptId=SCRIPT_ID \
--function=calendarDashboardSetup
This function:
- Registers time-driven triggers for
syncCalendarToBoatsetter()(every 30 minutes) - Registers triggers for
syncToSailo()andsyncToViator() - Initializes the dashboard ticket tracking system
Key Decisions and Trade-offs
Why not use service accounts? Service accounts have unlimited token lifetime but don't have calendar access delegation set up. User-mode OAuth with long-lived refresh tokens is the correct pattern here.
Why Testing mode was a silent killer: Google's Testing mode isn't documented prominently in token refresh code. Engineers