Fixing Permanent OAuth Token Expiration: Moving JADA's Google Cloud App from Testing to Production Mode
The Problem: A 7-Day Refresh Token Death Cycle
We've been stuck in a recurring authentication nightmare. Every week or so, our Google Calendar and Gmail integrations stop working because refresh tokens expire after exactly 7 days. The symptom: manual re-authentication required, downtime on calendar syncs, and angry captain/crew notifications failing to send. We'd patch it by re-authing, but the root cause was never addressed.
After digging into the Google Cloud Console, I found the culprit: the JADA app is configured in Testing mode, which is explicitly designed for development. Google hard-caps refresh token lifetime at 7 days in Testing mode as a security measure. The fix isn't code—it's moving the app to Production mode in the OAuth consent screen.
Why This Matters: Testing vs. Production Mode
Google Cloud's OAuth consent screen has two modes:
- Testing mode: Refresh tokens expire in 7 days. Meant for dev teams iterating quickly. No external verification needed.
- Production mode: Refresh tokens valid for 6 months (or until user revokes). Requires app verification but eliminates the refresh cycle.
We were operating under the assumption that re-authing was a temporary fix, but Testing mode is a permanent constraint. Every 7 days, the token would silently expire, and the next calendar write or email send would fail. The integration appears to work, then suddenly breaks.
Technical Details: What We're Authenticating
Our authentication flow touches three main systems:
- Google Calendar API (via Google Apps Script): Used by
CalendarSync.gsto sync Boatsetter bookings into JADA's master calendar. The trigger was never activated, but once fixed, it needs persistent auth. - Gmail API (via Apps Script): Sends captain/crew notifications for upcoming sails. Token revocation stops all outbound notifications.
- Google Sheets API (via Apps Script): Reads crew dispatch state from the JADA Internal sheet at
/Users/cb/Documents/repos/tools/state.json(exported from Sheets).
All three use the same OAuth 2.0 consent screen configuration. Moving to Production mode fixes all three simultaneously.
The Fix: Two Steps in Google Cloud Console
The actual fix requires exactly two clicks, but the prerequisites are critical:
Step 1: Verify Your App Identity (One-Time)
Google requires app verification before moving to Production. This involves:
- Confirming domain ownership (for
tech.queenofsandiego.comandjada-sailing.appspot.com) - Providing a privacy policy URL
- Describing how we use scopes (calendar, email, sheets)
Since this is an internal tool (crew/captain use only, no external users), we qualify for the "internal app" exemption and don't need full third-party verification. However, we still need to declare intent in the consent screen.
Step 2: Move Consent Screen to Production
Once verified, navigate to Google Cloud Console → APIs & Services → OAuth consent screen:
- Change the radio button from "Testing" to "Production"
- Save and publish
- Existing users must re-authenticate once; new tokens will be valid for 6 months
Infrastructure & Deployment Impact
This change affects the following resources:
- Google Cloud Project ID:
jada-sailing(managed via gcloud CLI) - OAuth 2.0 Client ID: Stored in the Google Cloud Console; no manual rotation needed
- Apps Script Deployments:
CalendarSync.gs(ID:AKfycbyXXXXXXXXX—exact ID in GAS editor settings)DashboardNotifier.gs(similar pattern)
- Scopes Required (in
appsscript.json):https://www.googleapis.com/auth/calendarhttps://www.googleapis.com/auth/gmail.sendhttps://www.googleapis.com/auth/spreadsheets
No changes needed to Lambda, DynamoDB, S3, or CloudFront. This is purely a Google Cloud OAuth configuration change.
Testing the Fix
After publishing to Production, verify with:
# Check current token scopes and expiry
gcloud auth list
gcloud auth application-default print-access-token
# In Google Apps Script editor, manually trigger CalendarSync:
# - Open CalendarSync.gs
# - Click Run → calendarDashboardSetup()
# - Check Execution log for success
# Verify calendar sync worked:
# - Add a test event in Boatsetter
# - Check JADA Internal calendar for iCal pull
Once activated, the GAS trigger calendarDashboardSetup() will run on a schedule (currently set to hourly) and pull Boatsetter events into the master calendar automatically.
Why We Didn't Catch This Sooner
The Testing mode constraint isn't obvious in day-to-day usage. The app works fine until token expiry hits. We had a workaround (re-auth), so the systemic issue stayed hidden. The real lesson: when you set up OAuth for production use, move it to Production mode immediately. Testing mode is for feature validation, not operational deployments.
What's Next
- Move OAuth consent screen to Production mode (2 clicks in Cloud Console)
- Re-authenticate all Apps Script deployments once (each will prompt for permissions once)
- Activate the CalendarSync trigger to auto-sync Boatsetter iCal
- Document the scopes and consent screen status in the engineering wiki to prevent this regression
- Monitor token refresh logs for the next 6 months to confirm tokens stay valid
After this change, we should see zero token expiration events for the next half-year, eliminating a major source of weekend notification failures.