```html

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.gs would not execute
  • Each fix required running gcloud auth application-default login or 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.gs contained the sync logic but the trigger was never activated
  • calendarDashboardSetup() and calendarSyncSetup() 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() and syncToViator()
  • 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