Fixing Permanent OAuth Token Expiration: Moving from Google Cloud Testing Mode to Production
The Problem: 7-Day Refresh Token Hard Cap
Our booking automation system was stuck in a cycle of OAuth re-authentication every 7 days. The root cause wasn't a code bug—it was infrastructure: our Google Cloud project remained in Testing mode, which enforces an automatic 7-day expiration on refresh tokens regardless of token configuration.
Every time a token expired, we'd manually re-run /Users/cb/Documents/repos/tools/reauth_jada_all.py, push the new token to our Lambda environment, and temporarily restore service. This pattern repeated monthly, consuming operational overhead and creating fragile outage windows.
The fix required moving the OAuth consent screen from Testing to Production—a seemingly simple change that unlocks indefinite refresh token lifespans.
Why Testing Mode Exists (And Why We Needed Out)
Google Cloud's Testing mode is designed for development: it limits token lifetime to 7 days, restricts token refresh to verified test users, and prevents scope creep. For a development team of 1–2 people, this is fine. For a production service serving hundreds of bookings monthly, it's a breaking constraint.
Moving to Production mode requires completing a security questionnaire, configuring a privacy policy URI, and publishing the app. Google then reviews the OAuth consent screen before issuing Production credentials. Critically, Production refresh tokens do not expire unless explicitly revoked.
Technical Steps to Production OAuth
1. Verify Current GCP Configuration
gcloud config list
# Output should confirm project=jada-sail-booking-prod
We verified the gcloud CLI had active credentials to the GCP project and confirmed we were targeting the correct project ID: jada-sail-booking-prod.
2. Access OAuth Consent Screen (Cloud Console Only)
The OAuth consent screen configuration is not available via API—it's UI-only in the Google Cloud Console. Navigate to:
APIs & Services > OAuth consent screen
https://console.cloud.google.com/apis/credentials/consent
Current state: Testing mode, with the following scopes configured:
https://www.googleapis.com/auth/calendar(Google Calendar API)https://www.googleapis.com/auth/gmail.modify(Gmail API)https://www.googleapis.com/auth/spreadsheets(Google Sheets API)https://www.googleapis.com/auth/script.deployments(Apps Script API)
3. Complete Required Fields for Production
Transitioning to Production requires:
- App name: "JADA Sailing Calendar & Booking Automation"
- User support email: jadasailing@gmail.com (already verified)
- Developer contact: jadasailing@gmail.com
- Privacy policy URI: https://tech.queenofsandiego.com/privacy (must be a publicly accessible HTTPS endpoint)
- Terms of service URI: Optional, but recommended for credibility
If these URIs don't yet exist, create them:
- Host a minimal privacy policy document at
/privacyon your public domain (tech.queenofsandiego.com) - Content should describe: what data is accessed (calendar events, email), why (booking automation), and how long it's retained
- CloudFront is already caching tech.queenofsandiego.com, so update the S3 origin or origin configuration to include the new page
4. Configure Scopes for Production Review
Each requested scope will be reviewed by Google. Provide clear justification for each:
- Calendar API: "Syncs booking events across multiple third-party platforms (Boatsetter, Viator, Sailo, GetMyBoat) into a single master calendar."
- Gmail API: "Sends automated booking confirmations and vendor notifications."
- Sheets API: "Logs booking data and crew assignments for business reporting."
- Apps Script API: "Triggers automated setup and deployment of booking automation flows."
5. Submit for Google Review
Once fields are complete, click "Publish to Production". Google typically reviews consent screens within 2–5 business days. During review, you can still use Testing mode credentials, so production service doesn't go down.
Updating Lambda & GAS Deployments Post-Approval
Once Google approves the Production consent screen:
Step 1: Generate new Production credentials
In Cloud Console, create a new OAuth 2.0 Client ID (or update the existing one to use Production mode). Download the JSON credentials file.
Step 2: Re-authenticate locally
# Run the reauth script to generate a new Production refresh token
python3 /Users/cb/Documents/repos/tools/reauth_jada_all.py
# This prompts browser OAuth flow using Production credentials
# Captured refresh token is stored locally
Step 3: Push to Lambda environment
# Deploy refresh token to Lambda environment variables
# Lambda function: jada-calendar-sync
# Environment variable: GOOGLE_REFRESH_TOKEN
# (Use AWS CLI or Lambda console directly)
Step 4: Redeploy Apps Script (Google Apps Script)
Apps Script deployments inherit the OAuth context of the user who deployed them. However, since we're using a unified service account token approach (via Lambda), redeploy the main Calendar Sync project:
# Push updated code and version
clasp push --rootDir /path/to/booking-automation-gas
# List deployments to verify
clasp deployments
# Ensure the main BookingAutomation deployment (v98+) includes calendarSyncSetup
# This function initializes all three sync triggers:
# - calendarSyncBoatsetter()
# - calendarSyncSailo()
# - calendarSyncViator()
Verification: Testing Token Persistence
Once deployed with Production credentials, validate that refresh tokens no longer expire:
# Check Lambda logs for successful calendar sync
aws logs tail /aws/lambda/jada-calendar-sync --follow
# Manually invoke calendar sync Lambda
aws lambda invoke \
--function-name jada-calendar-sync \
--payload '{"action":"sync_all"}' \
response.json
# Should complete without auth errors after 7+ days
Key Decision: Why Not Use a Service Account?
An alternative approach would be using a Google Cloud Service Account with a private key (never expires). However, this approach has downsides:
- Service accounts require domain-wide delegation to access user calendars, adding GCP setup complexity
- Calendar events would appear owned by the service account, not the user, breaking visibility in third-party platforms
- Email integrations (sending via Gmail) don't work well with service account credentials
Using a user-based OAuth token