Building a Distributed OAuth Re-Authentication System for Gmail API Scope Expansion
What Was Done
The JADA booking system faced a critical authentication gap: not all stored Google tokens had the gmail.compose scope required to programmatically draft and send emails. Rather than force users through a manual OAuth consent flow on every machine, we built a distributed re-authentication system that:
- Sends a single OAuth consent link via email (works from any device, any network)
- Watches the user's inbox for the OAuth callback URL
- Exchanges the authorization code for tokens with all required scopes
- Syncs updated tokens across Mac development environment and Lightsail production box
- Automatically updates Lambda environment variables without redeployment
This eliminated manual token management and the "localhost refused connection" friction that prevented users from re-authenticating when scope requirements changed.
Technical Details: The Three-Piece Architecture
1. The Re-Auth Server (/Users/cb/icloud-repos/tools/reauth_jada_all.py)
A lightweight Flask server that initiates the OAuth flow:
# Starts a listener on a dynamic port and launches the user's browser
# The OAuth redirect_uri points to localhost:PORT
# Because the user might be on a different device, the flow is:
# 1. Server starts on user's machine (Mac)
# 2. User clicks the emailed link on their phone or another device
# 3. Google redirects to the running server with the code parameter
# 4. Server captures it and extracts the authorization code
Key insight: We didn't assume the user could reach localhost from their approval device. Instead, the server redirects capture the code in the query string, then the user manually emails that redirect URL back to jadasailing@gmail.com. This email becomes the reliable command channel.
2. The Email Watcher (/Users/cb/icloud-repos/tools/watch_auth_email.py)
A long-running Gmail poller that:
- Connects to
jadasailing@gmail.comusing the existing unified token file (stored at~/.cache/jadasailing-token.json) - Polls the inbox every 5 seconds for emails matching the OAuth redirect pattern
- Extracts the
code=...parameter from the email body - Signals
finish_auth.pyvia a shared state file
Why email as the command channel? It's asynchronous, survives network blips, and requires zero infrastructure assumptions about user connectivity. If the watcher crashes, the user can re-send the email.
3. The Token Exchange Handler (/Users/cb/icloud-repos/tools/finish_auth.py)
Once the code arrives, this script:
- Exchanges the authorization code for access + refresh tokens using the OAuth client credentials stored in the reauth script
- Requests all required scopes:
https://www.googleapis.com/auth/gmail.readonly,https://www.googleapis.com/auth/gmail.compose,https://www.googleapis.com/auth/calendar - Writes the unified token file to
~/.cache/jadasailing-token.json(both Mac and Lightsail read this path) - Updates the Lambda environment variable
JADA_GOOGLE_TOKEN_PATHvia the AWS SDK (no redeployment needed) - Syncs the token to Lightsail by writing it to the shared home directory replica
Infrastructure: Token Scope Unification
The root cause was scope fragmentation. Legacy token files at ~/.cache/jada-token-gmail-full.json and ~/.cache/jada-token-unified.json had diverged in their capabilities:
jada-token-unified.jsonwas missinggmail.compose- Some tools expected it; others fell back to
jada-token-gmail-full.json, which was stale - New tools (draft generation, mass email) required
gmail.composebut the token hadn't been refreshed in months
The re-auth flow consolidates on the unified token. The OAuth request includes scope=https://www.googleapis.com/auth/gmail.readonly%20https://www.googleapis.com/auth/gmail.compose%20https://www.googleapis.com/auth/calendar, so a single token now satisfies all current and near-term needs.
Why This Pattern: Cross-Environment Token Sync
JADA infrastructure spans two environments with different constraints:
- Mac (development): Can't reach Google's token endpoint directly from certain networks; used for local testing, draft composition, manual admin tasks
- Lightsail (production): Runs the ticket runner, scheduled jobs, and Lambda-triggered workflows; has stable egress and whitelisting
When a token expires or scopes change, both need the update immediately. The unified token file at ~/.cache/jadasailing-token.json is the source of truth. The reauth script runs on Mac (user-initiated), but finish_auth.py also writes to Lightsail's home directory replica via the EC2-mounted iCloud bridge at ~/icloud-repos/. A simple rsync or direct write ensures Lightsail's Lambda can pick up the new token within seconds.
Key Decisions
Why not just ask the user to tap the consent link directly? Users might be on a phone with no way to copy/paste localhost URLs. Email makes it device-agnostic and adds a built-in delay (the user has time to read any warnings).
Why not use a central OAuth proxy server? That would require routing all token requests through a shared endpoint and managing credentials there. Keeping the flow local (on the user's own machine) eliminates a single point of failure and reduces credential surface area.
Why store the token in ~/.cache/ instead of a secrets directory? The .cache path is predictable, survives iCloud syncs (unlike ~/Documents, which is TCC-blocked on Mac), and is consistent across our production runbooks. Secrets for client credentials live separately in environment variables.
Deployment and Testing
The scripts were added to /Users/cb/icloud-repos/tools/ with corresponding ICM layer documentation:
CLAUDE.md— task map and scope boundaries (e.g., "don't rebuild if token scopes haven't changed")CONTEXT.md— architecture diagram, troubleshooting flowchart, and credential references
Testing involved:
- Running the reauth script with a 2-hour listener window and manually approving the OAuth flow
- Verifying the email watcher detected the callback URL from
jadasailing@gmail.com - Confirming the unified token file was updated and contained all three scopes
- Spot-checking the Lambda environment variable refresh via the AWS SDK
What's Next
Short term: Monitor token expiry and refresh cycles to ensure the unified token remains valid. Long term, consider:
- Automating the re-auth flow on a schedule (refresh tokens before they age 12 months)
- Building a health dashboard that flags scope mismatches before they break dependent tools
- Extending this pattern to other OAuth integrations (Stripe, Calendly) that may need scope updates