```html

Fixing the OAuth Token Rot: Moving JADA's Google Cloud App from Testing to Production Mode

We've been fighting the same OAuth battle for months. Every week, someone clicks through the Google consent screen again, grabs a fresh token, and we're good for another seven days. Then it dies. Again. The root cause? Our Google Cloud application has been stuck in Testing mode since inception, which hard-caps refresh token lifetime at seven days. This post walks through the permanent fix: publishing the app to Production mode and automating token refresh to eliminate manual re-authentication.

The Problem: Token Rot by Design

JADA's calendar sync, crew dispatch queries, and dashboard updates all depend on OAuth2 tokens issued by Google Cloud. The application service account and user-facing OAuth flows were configured in the Google Cloud Console, but the OAuth consent screen was never moved out of Testing status.

In Testing mode, Google intentionally expires refresh tokens after 7 days of inactivity. For a calendar system that needs to write bookings and read crew assignments continuously, this is a ticking time bomb. The symptom: sync failures, broken Lambda endpoints, and manual intervention every week.

The fix isn't a token refresh script—that's a band-aid. The fix is to publish the OAuth consent screen to Production, which allows refresh tokens to live for 6 months (or until explicitly revoked).

Technical Details: OAuth Consent Screen Transition

Step 1: Verify Current Configuration

First, confirm the gcloud CLI is authenticated to the correct Google Cloud project:

gcloud auth list
gcloud config get-value project
# Should return: jada-internal-ops (or your project ID)

Check the current OAuth consent screen status. Unfortunately, this setting lives in the Cloud Console UI only—there's no gcloud or API command to retrieve it directly. Navigate to:

Google Cloud Console → APIs & Services → OAuth consent screen

You'll see either "Testing" or "In production" next to your app name. If it says Testing, you'll also see a red warning: "Publishing status: Testing — Refresh tokens will expire in 7 days."

Step 2: Validate Required Fields

Before publishing, Google requires certain fields to be filled in the consent screen configuration:

  • App name: "JADA Internal Operations"
  • User support email: jadasailing@gmail.com
  • Developer contact information: At least one email (jadasailing@gmail.com)
  • Scopes: Must be declared and justified (we use calendar.events, sheets.readonly, etc.)
  • Privacy policy URI (optional but recommended for Production): https://tech.sailjada.com/privacy

These are already configured in our setup. Verify them by checking the OAuth consent screen page in the console.

Step 3: Move to Production (2 Clicks)

On the OAuth consent screen page, scroll down to "Publishing status." Click the "PUBLISH APP" button. Google will ask you to confirm—this moves the app out of Testing status immediately. No verification required if you're the project owner.

After publishing, the consent screen will show "In production" and the warning about 7-day token expiry disappears. Refresh tokens issued after this change will live for 6 months.

Automating Token Refresh: The Permanent Fix

Moving to Production solves token expiry, but we still need to handle token refresh gracefully. We created /Users/cb/Documents/repos/tools/reauth_jada_all.py to automate this process.

The Script: reauth_jada_all.py

This Python script manages OAuth2 refresh tokens across all JADA integrations:

#!/usr/bin/env python3
# Path: /Users/cb/Documents/repos/tools/reauth_jada_all.py
# Purpose: Refresh all OAuth2 tokens for JADA calendar, crew dispatch, and dashboard systems

import os
import json
import subprocess
from pathlib import Path
from datetime import datetime

# Token storage location
TOKEN_DIR = Path.home() / ".jada" / "tokens"
TOKEN_DIR.mkdir(parents=True, exist_ok=True)

SERVICES = {
    "google_calendar": {
        "token_file": TOKEN_DIR / "google_calendar_token.json",
        "scope": "https://www.googleapis.com/auth/calendar.events",
        "client_id": os.getenv("JADA_GOOGLE_CLIENT_ID"),
    },
    "google_sheets": {
        "token_file": TOKEN_DIR / "google_sheets_token.json",
        "scope": "https://www.googleapis.com/auth/spreadsheets.readonly",
        "client_id": os.getenv("JADA_GOOGLE_CLIENT_ID"),
    },
}

def refresh_token(service_name):
    """Refresh OAuth2 token using the refresh_token grant."""
    config = SERVICES[service_name]
    token_file = config["token_file"]
    
    if not token_file.exists():
        print(f"[ERROR] Token file not found for {service_name}: {token_file}")
        return False
    
    with open(token_file) as f:
        token_data = json.load(f)
    
    refresh_token_value = token_data.get("refresh_token")
    if not refresh_token_value:
        print(f"[ERROR] No refresh_token in {service_name}")
        return False
    
    # Call Google's token endpoint
    cmd = [
        "curl", "-s", "-X", "POST",
        "https://oauth2.googleapis.com/token",
        "-d", f"client_id={config['client_id']}",
        "-d", f"client_secret={os.getenv('JADA_GOOGLE_CLIENT_SECRET')}",
        "-d", f"refresh_token={refresh_token_value}",
        "-d", "grant_type=refresh_token",
    ]
    
    result = subprocess.run(cmd, capture_output=True, text=True)
    response = json.loads(result.stdout)
    
    if "error" in response:
        print(f"[ERROR] Token refresh failed for {service_name}: {response['error']}")
        return False
    
    # Update token file with new access_token and expiry
    token_data["access_token"] = response["access_token"]
    token_data["expires_in"] = response.get("expires_in", 3600)
    token_data["refreshed_at"] = datetime.now().isoformat()
    
    with open(token_file, "w") as f:
        json.dump(token_data, f, indent=2)
    
    print(f"[OK] Refreshed {service_name} at {token_data['refreshed_at']}")
    return True

if __name__ == "__main__":
    print(f"[*] JADA OAuth Token Refresh — {datetime.now().isoformat()}")
    
    for service in SERVICES.keys():
        refresh_token(service)
    
    print("[*] Done.")

Make the script executable:

chmod +x /Users/cb/Documents/repos/tools/reauth_jada_all.py

Set environment variables for client credentials (store these securely in your shell profile or a .env file):

export JADA_GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
export JADA_GOOGLE_CLIENT_