Fixing OAuth Token Expiry in Google Apps Script: Moving from Testing to Production Mode

The Problem

Our Google Apps Script (GAS) project managing calendar synchronization kept requiring manual re-authentication every 7 days. The root cause wasn't a bug in our code—it was a configuration issue in Google Cloud Console: the OAuth consent screen was stuck in Testing mode, which artificially caps refresh token lifetime to 7 days regardless of your token configuration.

This affected three critical workflows:

  • CalendarSync.gs — syncing Boatsetter iCal bookings into JADA Internal calendar
  • Lambda calendar endpoint — reading crew dispatch events from Calendar API
  • Viator/Sailo supplier integrations — blocking booked dates across multiple platforms

Every 7 days, Gmail OAuth and Calendar API scopes would revoke, breaking all downstream automation until we manually clicked through the Google login flow again.

Root Cause: Testing vs. Production Mode

Google Cloud's OAuth consent screen has two modes:

  • Testing: Unlimited test users, but refresh tokens expire in 7 days. Intended for development only.
  • Production: Requires user consent verification and app review, but refresh tokens are valid indefinitely (until user revokes).

Our project was configured as Testing because the initial setup was done during development and never transitioned to Production. This is a common gotcha—the default state works fine locally, so it stays that way until automation breaks in production.

Technical Details: What Needed to Change

Step 1: Verify Current OAuth Configuration

First, we verified the current state via CLI:

gcloud auth list
gcloud config get-value project
gcloud iam service-accounts list

This confirmed we had CLI access to the Google Cloud project. We then checked the OAuth app's current consent screen status through the Cloud Console (this is UI-only; there's no API endpoint to programmatically read the consent screen mode).

Step 2: Identify Required Scopes

Our GAS project uses these OAuth scopes in appsscript.json:

  • https://www.googleapis.com/auth/calendar — read/write calendar events
  • https://www.googleapis.com/auth/gmail.readonly — read booking confirmation emails
  • https://www.googleapis.com/auth/spreadsheets — read/write booking state in Sheets

All three are considered "sensitive" scopes by Google, meaning Production mode requires explicit review. However, since this is an internal tool used only by JADA staff (not public-facing), the review process is straightforward.

Step 3: Prepare the OAuth Consent Screen for Production

The transition requires these fields in Cloud Console → OAuth consent screen:

  • App name: "JADA Sailing Calendar Sync"
  • User support email: jadasailing@gmail.com
  • Scopes: calendar, gmail.readonly, spreadsheets (all marked as sensitive)
  • Developer contact information: jadasailing@gmail.com

Critically, we do not need to publish to Google's public app gallery. We're using the "Internal" user type, which means only JADA staff can access it—Google doesn't require the same level of scrutiny for internal apps.

Step 4: Update Token Storage and Refresh Logic

Our existing token refresh mechanism in /Users/cb/Documents/repos/tools/reauth_jada_all.py works correctly; it just needs to be run once after Publishing to Production. The script handles:

  1. Initiating the OAuth flow (user clicks the login link)
  2. Capturing the authorization code from the redirect
  3. Exchanging the code for an access token + refresh token
  4. Storing the refresh token in AWS Secrets Manager at jada/google/refresh_token
  5. Syncing the token to the Lightsail server running the calendar Lambda

The Python script uses the google-auth-oauthlib library to handle the OAuth 2.0 authorization code flow. Once Production mode is enabled and the consent screen is updated, the token generated by this script will have an indefinite lifetime (until manually revoked).

Step 5: Update Lambda Environment Variables

The Lambda function at arn:aws:lambda:us-east-1:ACCOUNT_ID:function:calendar-sync reads the refresh token from environment variable GOOGLE_REFRESH_TOKEN. After obtaining a new token in Production mode, we update this via:

aws lambda update-function-configuration \
  --function-name calendar-sync \
  --environment Variables={GOOGLE_REFRESH_TOKEN=<new_token>,GOOGLE_CLIENT_ID=<id>,GOOGLE_CLIENT_SECRET=<secret>}

Note: The CLIENT_ID and CLIENT_SECRET don't change; only the REFRESH_TOKEN is new.

Why This Approach

  • Permanent fix vs. band-aid: We fixed the root cause (Testing mode) instead of just re-authing every 7 days.
  • No code changes: Our existing GAS and Lambda code works unchanged; this is purely a Google Cloud configuration change.
  • Internal-only scope: We don't need full public app review; Google's internal app path is faster.
  • Minimal friction: Once transitioned, no further manual intervention is needed—the refresh token stays valid indefinitely.

Implementation Path

  1. Navigate to Google Cloud Console → APIs & Services → OAuth consent screen
  2. Update app name, support email, and developer contact (jadasailing@gmail.com)
  3. Ensure all three scopes (calendar, gmail.readonly, spreadsheets) are listed as "sensitive"
  4. Set user type to "Internal"
  5. Click "Publish to Production" — Google will verify the app is internal and approve in minutes
  6. Run python3 reauth_jada_all.py to generate a new refresh token valid indefinitely
  7. Update Lambda environment variable with the new refresh token
  8. Test the calendar sync endpoints to confirm all scopes work

Next Steps

After Production mode is enabled and the new token is deployed:

  • Verify CalendarSync.gs can write to JADA Internal calendar without token expiry
  • Confirm Lambda calendar endpoint returns events for all crew dispatch queries
  • Test Boatsetter iCal sync trigger (activate calendarDashboardSetup() in GAS editor)
  • Monitor Secrets Manager refresh token age to catch any future issues

This change eliminates a recurring operational headache and ensures our booking automation runs reliably without manual re-authentication cycles.