Rebuilding JADA's Google OAuth Re-authentication: Email-Driven Token Refresh with Scope Isolation
What Was Done
JADA's charter management system had accumulated technical debt around Google OAuth token management. The Google API tokens — needed for Gmail draft creation, calendar reads, and contact management — were expiring and losing required scopes (like gmail.compose) on refresh cycles. This session rebuilt the entire OAuth re-authentication pipeline with a deterministic, email-driven code exchange that doesn't require manual browser interaction after the initial consent flow.
Created three new Python utilities in ~/icloud-repos/tools/:
reauth_jada_all.py— Main orchestrator that launches the OAuth consent flow and holds a local server listener for the redirect callbackwatch_auth_email.py— Background watcher that monitors thejadasailing@gmail.cominbox for the OAuth authorization code, extracts it from the callback URL, and writes it to a named pipefinish_auth.py— Completes the token exchange, validates scopes, writes tokens to the correct file locations, and performs health checks
The Problem: Scope Creep and Token Fragmentation
Legacy JADA had two separate token files with incomplete scopes:
- A "unified" token that had read-only calendar/contacts scopes but lacked
gmail.compose - An older "gmail-full" token that worked sporadically but had inconsistent scope coverage
When the JADA draft-automation system needed to create Gmail drafts programmatically (for charter confirmations, waiver summaries, and guest communications), the compose scope was missing. Refreshing tokens dropped scopes entirely, making the tokens stale and forcing manual re-authorization in the browser — not sustainable for a 24/7 automated system.
Technical Architecture: Multi-Stage Email-Driven OAuth
Instead of expecting CB to babysit a browser consent screen, the new flow uses Gmail itself as the OAuth callback transport:
1. reauth_jada_all.py opens the OAuth consent URL (in Opera/Safari)
→ User grants access to required scopes
→ Google redirects to http://localhost:8888/?code=AUTH_CODE&state=...
2. Google's redirect hits the local listener, but no server-side code exchange yet.
→ The authorization code is in the URL and visible in browser history.
3. watch_auth_email.py polls the Gmail inbox for the OAuth notification email
→ Gmail always sends "A new device has accessed your Google Account"
→ Extract the redirect URL from that email (or search recent messages for the code)
4. finish_auth.py uses the code to call /token endpoint and exchange for access/refresh tokens
→ Validates that required scopes (gmail.compose, calendar, contacts) are present
→ Writes to the canonical token file at the location expected by downstream tools
→ Runs smoke tests (Gmail API "get profile" call) to confirm the token works
The flow is deterministic and scriptable — no human polling a browser. If the user is away from the machine, they can grant consent from their phone, and the email-watching loop continues polling until the notification arrives.
Infrastructure: Token Files, Scope Validation, and Retry Logic
Token File Locations
The reauth script validates and writes to two locations:
~/.google/credentials/unified-token.json— Primary token file for calendar/contacts/compose operations~/.google/credentials/gmail-full-token.json— Legacy location, kept for backwards-compatibility fallback reads
Both files follow the standard Google OAuth2 session token structure:
{"access_token": "...", "refresh_token": "...", "expires_in": 3599, "scopes": ["..."]}
Scope Validation
After token exchange, finish_auth.py validates that the returned token includes all required scopes:
REQUIRED_SCOPES = [
"https://www.googleapis.com/auth/gmail.compose",
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/contacts"
]
def validate_scopes(token_dict):
returned = set(token_dict.get("scopes", []))
for scope in REQUIRED_SCOPES:
if scope not in returned:
raise ValueError(f"Missing required scope: {scope}")
If scopes are missing, the re-auth fails loudly rather than silently writing a broken token downstream.
Retry and Polling Intervals
watch_auth_email.py runs a configurable polling loop (default: 180 minutes = 3 hours) with 45-second intervals between Gmail checks. The loop is resilient to transient 429 (rate limit) and 5xx errors, backing off 10 seconds on retry. If the authorization code isn't found in the inbox within the window, the tool exits with a clear error message pointing the user to check the browser tab for consent errors.
Key Decisions: Why This Approach?
Email-Driven Callback Instead of Browser-Based
A naive approach: have the local redirect handler immediately POST back to a web service with the code. Problem: requires a public HTTPS endpoint, adds attack surface, and breaks if CB is off-network. Advantage of email: it's always available, uses an existing authenticated channel (the JADA inbox), and gives CB audit trail of the re-auth event.
Separate Token Files for Legacy Compatibility
Instead of atomically replacing tokens in-place, the system writes to the primary location and keeps a secondary for fallback. Old dashboards/scripts that hardcode the legacy path still work, but new systems prefer the unified token. This bought time to migrate without a flag day.
Stateless Tools Chained via Named Pipes
Rather than having one mega-script handle all three stages, each tool does one job and hands off via a FIFO. This lets the user monitor each stage independently (watch the browser, tail the email watcher logs, then check token validity) and makes testing easier — you can mock an email or inject a code directly into the named pipe.
What's Next
- Token Refresh Automation — Implement a background service (launchd plist on macOS, systemd timer on Linux) that refreshes tokens 7 days before expiry, preventing unexpected auth failures in the charter confirmation workflow.
- Scope Drift Monitoring — Log token scopes on every read; alert if a refresh silently dropped
gmail.composeagain. - Multi-Account Support — Generalize the flow to handle re-auth for crew Gmail accounts (captain updates, insurance documents), not just the JADA house account.
- Audit Trail Consolidation — Pipe re-auth events into the FIRES incident ledger for compliance and SLA tracking.