Fixing the 7-Day OAuth Token Expiry: Moving Google Calendar Auth from Testing to Production
We've been fighting the same OAuth battle for months. Every seven days, the Google Calendar integration fails silently, forcing manual re-authentication through the Cloud Console. The root cause? Our Google Cloud OAuth app was stuck in Testing mode, which hard-caps refresh token lifetime at seven days. This post walks through the permanent fix: publishing the app to Production and implementing persistent token rotation.
The Problem: Testing Mode Token Limits
Google's OAuth 2.0 consent screen has two states:
- Testing: Refresh tokens expire after 7 days of inactivity. Perfect for development, terrible for production services.
- Production: Refresh tokens are valid indefinitely (until revoked). Required for any service that needs persistent, unattended access.
Our Lambda functions that sync Boatsetter bookings to JADA Internal calendar were hitting this wall every week. The symptom: calendar writes fail, dashboards go stale, and no one knows why until the next manual verification.
Technical Details: OAuth Consent Screen Configuration
The fix requires moving the OAuth consent screen from Testing to Production. Here's the exact process:
Step 1: Verify Current OAuth App Status
gcloud auth list
# Confirms you're authenticated to the Google Cloud project
gcloud config get-value project
# Verify the project ID (should match your JADA deployment project)
The OAuth consent screen configuration lives in the Cloud Console UI only — there's no API endpoint to query or update it. You'll navigate to:
https://console.cloud.google.com/apis/consent
Look for the "Publishing status" field. If it says "Testing," you need to change it to "In production."
Step 2: Complete Required Scopes Declaration
Before publishing, Google requires you to declare every OAuth scope your app uses. For our calendar sync workflow, that includes:
https://www.googleapis.com/auth/calendar— Read/write calendar eventshttps://www.googleapis.com/auth/gmail.readonly— Read booking confirmation emails from Boatsetterhttps://www.googleapis.com/auth/drive.readonly— Access shared drive for state management
These should already be declared in your /Users/cb/Documents/repos/tools/reauth_jada_all.py script under the SCOPES variable. Verify they match what's declared in the Cloud Console consent screen configuration.
Step 3: Add Required User Support Contact
Production apps require a support email. In the Cloud Console, set:
- App name: "JADA Sailing Calendar Sync"
- Support email: jadasailing@gmail.com (or your ops contact)
- Developer contact: Same email or your primary GCP project owner
Step 4: Publish to Production (2 Clicks)
In the Cloud Console consent screen editor, locate the "Publishing status" dropdown and select "In production." Google will validate your scopes and support contact, then immediately update the consent screen. No manual review queue, no waiting — it's instant.
Infrastructure: Token Storage and Rotation
Once the consent screen is in Production, new refresh tokens will be indefinite. But here's the critical piece: token management at the Lambda layer.
Current Token Storage Pattern
Tokens are currently stored in two places:
- Local dev machine:
~/.config/gcloud/application_default_credentials.json(for local testing) - Lightsail server:
/home/ubuntu/jada-tokens/(synced via S3 state bucket)
Your Lambda function reads the token from an environment variable set at deploy time or pulled from AWS Secrets Manager. The reauth script at /Users/cb/Documents/repos/tools/reauth_jada_all.py updates this token by:
# Pseudocode from the reauth script
1. Run gcloud auth application-default login (interactive browser flow)
2. Read refreshed token from ~/.config/gcloud/application_default_credentials.json
3. Upload to S3: s3://jada-state/tokens/calendar_oauth.json
4. Trigger Lambda environment variable update or Secrets Manager sync
5. Verify Lambda can reach Google Calendar API
Lambda Configuration
Your Lambda function should read the token from AWS Secrets Manager (not hardcoded environment variables). Example pattern:
import boto3
import json
secrets_client = boto3.client('secretsmanager')
def get_calendar_token():
try:
response = secrets_client.get_secret_value(
SecretId='jada-calendar-oauth-token'
)
return json.loads(response['SecretString'])
except Exception as e:
# Fall back to environment variable for backward compatibility
return json.loads(os.getenv('CALENDAR_TOKEN', '{}'))
def lambda_handler(event, context):
token = get_calendar_token()
# Use token for calendar API calls
...
This decouples token storage from code deployment — you can update the token without redeploying Lambda.
Key Decisions: Why This Approach
- Production consent screen over service account: Service accounts can't access Gmail or Google Drive (needed for booking emails and state files). We need user-delegated OAuth.
- Secrets Manager over S3: S3 state bucket is readable by multiple services; Secrets Manager provides better access control and rotation hooks.
- Indefinite refresh tokens with manual rotation: Even in Production mode, you should rotate tokens quarterly as a security best practice. The reauth script makes this a one-command operation.
- No automatic token refresh logic in Lambda: Keep it simple. Let Google's token refresh happen naturally; if it fails, the reauth script is your recovery path.
Immediate Action Items
- Navigate to
https://console.cloud.google.com/apis/consentand change "Publishing status" to "In production" - Add support email (jadasailing@gmail.com) to the consent screen configuration
- Run the reauth script once to generate a fresh indefinite refresh token:
python3 /Users/cb/Documents/repos/tools/reauth_jada_all.py - Verify the new token is synced to Lightsail and Lambda can invoke calendar API
- Set up a quarterly token rotation reminder (e.g., first Monday of every quarter)
What's Next
Once this is deployed, calendar sync will be persistent and reliable. The next phase is addressing the related issue: the calendarDashboardSetup() trigger in Google Apps Script (GAS) for Boatsetter iCal sync is still not activated. That's a separate ticket, but the OAuth fix unblocks it — once tokens are Production-grade, the GAS script can write to your calendar without expiry issues.