Fixing the 7-Day OAuth Token Expiration: Moving Google Calendar Sync from Testing to Production

We've been caught in a frustrating cycle: every week, the Google Calendar integration breaks because refresh tokens expire after 7 days. The root cause wasn't a code bug—it was an architectural oversight in how we configured the Google Cloud OAuth consent screen. Here's how we identified the issue, what we did about it, and why this fix is permanent.

The Problem: Testing Mode Token Limits

Our Google Cloud project for JADA calendar integrations was set to Testing mode on the OAuth consent screen. This mode is intentionally restrictive: Google automatically revokes refresh tokens after 7 days to prevent long-lived tokens in test environments. We were band-aiding this by re-authing every week—manually clicking through the OAuth flow, generating a fresh token, and pushing it to Lambda. Unsustainable.

The fix required moving the consent screen configuration from Testing to Production mode. This isn't a deployment step; it's a configuration change in Google Cloud Console that tells Google: "This app is legitimate, we're the owners, and we want long-lived tokens."

Technical Details: OAuth Consent Screen Configuration

Here's what we discovered during the audit:

  • Current state: The OAuth consent screen in Google Cloud Console (project: `jada-calendar-sync`) was in Testing mode with only test users whitelisted. This is fine for development but incompatible with production token longevity.
  • Scopes required: We're requesting `calendar.events`, `calendar.readonly`, and `gmail.modify` scopes across multiple integrations (JADA Internal calendar, Boatsetter sync, crew dispatch updates). Testing mode doesn't cap scope requests, but it does cap token lifetime.
  • API enablement: The Google Calendar API and Gmail API were already enabled in the project. No API changes needed.

The OAuth consent screen configuration lives in Google Cloud Console and isn't accessible via API—it's UI-only. Moving to Production mode requires:

  1. Setting the app type to "Web application" (already correct)
  2. Configuring the app name, support email, and privacy policy URL
  3. Listing all required scopes transparently
  4. Submitting the configuration to Google for review (if not in a Google Workspace org; we're not affected since we're Workspace admins)
  5. Changing the consent screen status from "Testing" to "Production"

Infrastructure: Token Storage and Rotation

Our token architecture spans three layers:

  • Local development: Token files stored in `/Users/cb/Documents/repos/tools/` (e.g., `google_calendar_token.json`, `gmail_token.json`) with scopes embedded in the filename for clarity.
  • Lambda environment: Tokens passed as environment variables to the Calendar and Gmail Lambdas in AWS. These are deployed via the infrastructure stack, not hardcoded in code.
  • Lightsail server: A copy of tokens synced via `rsync` for any background jobs or CLI tools that need calendar access outside Lambda.

The reauth script we created—/Users/cb/Documents/repos/tools/reauth_jada_all.py—handles the entire flow:

#!/usr/bin/env python3
# Iterates through each Google service (Calendar, Gmail)
# Triggers OAuth flow locally via browser redirect
# Saves tokens to appropriately-scoped files
# Syncs tokens to Lightsail if SSH keys are available
# Pushes tokens to Lambda environment variables via AWS CLI

With Production mode enabled, we'll be able to run this reauth flow once every 6 months (or on-demand) instead of weekly. Refresh tokens in Production mode don't have the 7-day hard cap.

Calendar Sync Mechanics: Why Boatsetter Didn't Auto-Sync

While we were debugging the OAuth layer, we discovered a separate blocker: the Boatsetter iCal sync was written but never activated.

The code exists in CalendarSync.gs (our Google Apps Script file bound to the JADA Internal sheet). It includes the Boatsetter iCal URL and a sync function that:

  1. Fetches events from the Boatsetter iCal feed
  2. Parses event details (date, time, guest count, boat assignment)
  3. Creates corresponding events in JADA Internal calendar
  4. Tags them with a source identifier for deduplication

But the trigger was never set up. To activate it, we need to:

  1. Open CalendarSync.gs in the Apps Script editor
  2. Run the function calendarDashboardSetup() once (it creates time-driven triggers for hourly syncs)
  3. Verify the Gmail OAuth token is still valid (it wasn't—it had been revoked)

Once OAuth is in Production mode and tokens are rotated, this sync will run reliably every hour, eliminating manual calendar blocking for Boatsetter bookings.

Integration Points Affected

  • Boatsetter → JADA Internal: iCal feed via `CalendarSync.gs` trigger (blocked until GAS setup + OAuth refresh)
  • JADA Internal → Sailo: Same mechanism as Boatsetter—relies on JADA iCal export and GAS trigger
  • JADA Internal → Viator: Currently blocked pending API integration review (separate ticket; not OAuth-related)
  • Lambda Calendar API → DynamoDB crew dispatch: Uses Calendar Lambda to cross-reference events with crew assignments (this works; OAuth expiration breaks the Lambda invocation only)

Deployment Plan

Immediate (today):

  • Move OAuth consent screen from Testing to Production in Google Cloud Console (2 clicks)
  • Run reauth_jada_all.py to generate fresh tokens with Production-mode longevity
  • Sync tokens to Lightsail: rsync -avz /Users/cb/Documents/repos/tools/google_*.json lightsail_user@lightsail_ip:/path/to/tokens/
  • Push tokens to Lambda environment variables via AWS CLI
  • Test Calendar Lambda endpoint with the new token

Short-term (this week):

  • Activate calendarDashboardSetup() in the Apps Script editor to enable Boatsetter sync triggers
  • Verify the first sync run and check JADA Internal calendar for Boatsetter events
  • Manually block May 30 Boatsetter event on Viator pending API integration resolution

Long-term:

  • Token rotation is now on a 6-month schedule instead of weekly, reducing operational burden
  • Once Viator API integration is confirmed, automate blocking across all platforms

Why This Time It's Permanent

The difference: we're addressing the root cause (Testing mode token cap) instead of the symptom (weekly reauth). Production mode tells Google we're a legitimate, long-lived application