Fixing Permanent OAuth Token Expiry: Moving from Google Cloud Testing Mode to Production
The Problem: 7-Day Token Rot in Testing Mode
We've been chasing OAuth token expiry failures for weeks, re-authing on a 7-day cycle and patching the symptom rather than the root cause. The real issue: our Google Cloud project's OAuth consent screen is stuck in Testing mode, which forces refresh tokens to expire after exactly 7 days per Google's OAuth 2.0 security model. This affects every service that touches our Google APIs: the calendar sync Lambda, Apps Script deployments, and the unified token system we built last sprint.
The fix isn't a code change — it's a one-time infrastructure move from Testing to Production on the OAuth consent screen. Once done, refresh tokens live for 6 months, eliminating the re-auth treadmill entirely.
Technical Details: Why Testing Mode Kills Tokens
Google's OAuth implementation treats Testing and Production consent screens differently:
- Testing mode: Allows up to 100 test user accounts, refresh tokens issued with a 7-day TTL. Designed for development iteration.
- Production mode: Tokens valid for 6 months (or until revoked). Requires app verification, but we're an internal tool — we skip that step.
Our setup uses these scopes across multiple services:
https://www.googleapis.com/auth/calendar
https://www.googleapis.com/auth/gmail.readonly
https://www.googleapis.com/auth/gmail.send
https://www.googleapis.com/auth/spreadsheets
https://www.googleapis.com/auth/script.external_request
Each token refresh (whether manual or via Lambda's token refresh logic in `/Users/cb/Documents/repos/tools/reauth_jada_all.py`) still generates a new refresh token with the same 7-day window if Testing mode is active. No amount of code fixes changes this.
Infrastructure: Moving to Production
The migration involves exactly two steps in Google Cloud Console:
- Access the OAuth consent screen:
- Project: JADA Sailing (the unified Apps Script / Lambda service account project)
- Path: APIs & Services → OAuth consent screen
- Current status: Testing mode with ~5 test users added
- Click "Make application live": This button appears in the Testing mode UI. It transitions the app to Production without requiring external app verification (since this is a private, internal tool with only Carole, Gene, and CB as users).
That's it. No code deployment needed. No Lambda updates. No token re-generation. The next token issued after this change will carry a 6-month TTL.
Why We're Doing This Now, Not Earlier
When we initially built the unified token system (roughly 2 weeks ago), the OAuth consent screen was provisioned but never verified as Production-ready. The assumption was "we'll fix that when we have time" — but Testing mode's 7-day window made that a week-1 blocker, not a week-3 task. The symptom (expired tokens) was easier to debug than the root cause (testing mode), so we kept re-authing.
That's a common pattern in infrastructure work: the path of least resistance (patching tokens) is visible and feels productive, while the structural fix (changing OAuth mode) requires a moment of clarity and one trip to the Cloud Console.
Current Token Flow (Post-Fix)
After moving to Production, the token lifecycle becomes:
1. User runs reauth_jada_all.py (or Carole triggers via email)
↓
2. Script authenticates to Google OAuth endpoint
↓
3. Google issues new refresh token (6-month TTL, not 7-day)
↓
4. Token synced to Lightsail Lambda server via SSH
5. Lambda stores in /tmp/JADA_all_tokens.json
↓
6. Calendar sync, email, and Apps Script operations use unified token
↓
7. Token remains valid for ~180 days before next re-auth needed
The Lambda calendar endpoint (invoked via API Gateway at `/api/calendar/{event_id}`) uses this token transparently. No code changes to `/opt/lambda/calendar_handler/index.js` needed.
Related Fixes: Activating Calendar Sync Triggers
While we're addressing token expiry, we also need to activate the Boatsetter → JADA calendar sync. This is a separate but adjacent issue:
- File: `/Users/cb/Documents/repos/apps_script/CalendarSync.gs`
- Status: Code written, Boatsetter iCal URL configured, but trigger never activated
- Action: Run `calendarDashboardSetup()` once in the Apps Script editor (not via clasp, direct execution)
- Effect: Creates a time-based trigger that polls Boatsetter's iCal feed every 6 hours and syncs new events to JADA Internal calendar
This was blocked on expired calendar OAuth scopes. Once tokens move to Production, the unified token will have calendar write permissions, and the sync trigger will start working immediately.
Key Decisions
- Why not request app verification? Unnecessary overhead for an internal tool. Production mode works fine without external verification as long as the app isn't public-facing.
- Why now? Token re-auth is currently blocking calendar sync, Viator API integration, and creating manual work every 7 days. The cost of one Cloud Console trip is far lower than the ongoing friction.
- Why unified token, not service account? A few services (notably Apps Script web apps) require user-delegated credentials, not service accounts. The unified user token approach handles both.
What's Next
- Move OAuth consent screen to Production (2 minutes, Cloud Console only)
- Run `reauth_jada_all.py` to issue a new refresh token with 6-month TTL
- Sync new token to Lightsail via `rsync`
- Verify Lambda calendar endpoint works (test via `/api/calendar/{event_id}`)
- Activate `calendarDashboardSetup()` trigger in Apps Script editor
- Monitor Boatsetter iCal feed sync for next 48 hours (should see May 30 booking auto-sync to JADA Internal)
- Document this in ops runbook so we don't re-learn it in 6 months
After this, we're off the re-auth treadmill. Tokens valid for 180 days, calendar sync automated, and Boatsetter bookings flowing to JADA without manual intervention.
```