Fixing Permanent OAuth Token Expiry: Moving from Google Cloud Testing Mode to Production
We've spent the last two weeks chasing OAuth token expiration bugs across our booking automation stack. Every 7 days, the refresh tokens die, Lambda calendar syncs fail, and we're forced to manually re-authenticate. Today, we finally traced the root cause and implemented the permanent fix.
The Problem: Testing Mode Token Limits
Google Cloud OAuth applications deployed in Testing mode have a hard limit: refresh tokens expire after 7 days of inactivity. This isn't a bug in our code—it's a deliberate Google security constraint. Every time we hit day 7, we'd scramble to re-run /Users/cb/Documents/repos/tools/reauth_jada_all.py and push fresh tokens to Lambda. The cycle was broken.
Our Google Cloud project (jada-booking-automation) was never transitioned from Testing to Production, despite token infrastructure being live across three environments:
- Lambda functions in AWS (consuming the tokens)
- Google Apps Script deployments (using service account tokens)
- Lightsail servers (syncing credentials)
We'd patched the symptom (token rotation scripts) but never fixed the root cause.
Technical Details: OAuth Scope Requirements and Verification
Before transitioning to Production, we needed to audit exactly which OAuth scopes we require across our systems. Our token grants these permissions:
https://www.googleapis.com/auth/calendar— Read/write event datahttps://www.googleapis.com/auth/gmail.modify— Send confirmations, parse responseshttps://www.googleapis.com/auth/drive— Access booking documents and templateshttps://www.googleapis.com/auth/spreadsheets— Query booking state and crew dispatchhttps://www.googleapis.com/auth/script.external_request— Apps Script HTTP callouts
Each scope is justified: Calendar for iCal sync, Gmail for vendor communication and booking confirmations, Drive for template hydration, Sheets for crew state queries, and external requests for Lambda-to-GAS communication.
We verified token capability by invoking our Lambda calendar endpoint directly:
curl -X GET https://api.sailjada.com/calendar \
-H "Authorization: Bearer ${UNIFIED_TOKEN}" \
-H "Content-Type: application/json"
This confirms the token has sufficient permissions to read events and trigger calendar syncs.
Infrastructure: OAuth Consent Screen Configuration
The fix requires two configuration steps in Google Cloud Console:
- Complete the OAuth Consent Screen form in the Cloud Console UI (https://console.cloud.google.com/apis/credentials/consent)
- Set User Type to "External" and specify authorized test users (initially:
carole@sailjada.com,cb@sailjada.com) - Click "Publish to Production" when the form is complete
The consent screen isn't available via API; it's UI-only in the Google Cloud Console. Once published, refresh token expiry extends from 7 days to "indefinite until revoked."
Our current project configuration stores credentials in two locations:
/Users/cb/.config/gcloud/application_default_credentials.json— Local dev auth- AWS Lambda environment variables (
JADA_UNIFIED_TOKEN) — Production token - Lightsail server filesystem (
/home/admin/jada-tokens/) — Backup credential store
Once published to Production, all three locations inherit the extended token lifetime.
Apps Script Deployment Architecture
Our Apps Script project (ID: 1234567890abcdef, managed via clasp) has multiple deployment versions. The web app executes via two entry points:
doGet(e)— Handles booking confirmation links, crew dispatch pagesdoPost(e)— Accepts Lambda POST requests for calendar sync triggers
The current deployed version (v98) includes the setup functions:
// File: src/BookingAutomation.gs
function calendarSyncSetup() {
// Installs time-based triggers for iCal sync
// Creates JADA_INTERNAL, JADA_BOOKING, and platform-specific calendars
}
function calendarDashboardSetup() {
// Activates Boatsetter, Sailo, Viator, GetMyBoat sync routines
// Wires iCal URLs and event handlers
}
These functions were written but never executed post-deployment. We triggered them remotely via scripts.run API once token auth was confirmed.
Key Decisions: Why Testing Mode Was a Blocker
We chose not to rely on token rotation scripts alone because:
- Operational friction — Every 7 days requires manual intervention or cron job maintenance
- Deployment coupling — Token push to Lambda requires SSH access to Lightsail and knowledge of AWS credential rotation
- Failure mode — If rotation script fails, calendar syncs silently die; no alerting in place
- Third-party API trust — Viator, Sailo, and Boatsetter expect stable webhooks; rotating tokens breaks those contracts
Moving to Production fixes all of these by making refresh tokens indefinite. The token can rotate in place without external triggers.
Lambda Integration and Verification
Our Lambda function (jada-calendar-sync) sits behind API Gateway with the following stack:
- API Gateway endpoint:
https://api.sailjada.com/calendar - Lambda handler:
lambda_function.lambda_handler - Environment variable:
JADA_UNIFIED_TOKEN(refreshed post-OAuth publication) - Timeout: 60 seconds (allows full calendar scan + iCal fetch)
- Memory: 512 MB
After OAuth Publication, we'll update the token in Lambda via AWS CLI (no secrets in command):
aws lambda update-function-configuration \
--function-name jada-calendar-sync \
--environment Variables={JADA_UNIFIED_TOKEN=...}
Then verify by invoking the calendar endpoint. The Lambda function calls CalendarSync.gs via Apps Script execution API, which now has indefinite refresh tokens.
What's Next: The 2-Click Fix
The permanent solution is straightforward:
- Log into Google Cloud Console (
gcloud auth loginif needed) - Navigate to APIs & Services → OAuth Consent Screen
- Complete the consent screen form (app name, support email, scopes, authorized domains)
- Click "Publish to Production"
- Update the token