Phone-Friendly OAuth Flow: Bypassing Localhost Redirect Constraints
The Problem
Standard OAuth 2.0 redirect-based flows assume the user's browser and the token-receiving service are on the same machine. When initiating OAuth from a phone to grant Gmail/Calendar access to a Mac-based service, the redirect URI http://localhost points to the phone itself — which has no listening HTTP server. Result: "Safari cannot connect to server" error, with the precious authorization code trapped in the URL bar and no automated way to extract it.
In our case, this blocked jadasailing@gmail.com re-authentication for email and calendar automation across the JADA estate (sailing charters, crew dispatch, billing). The existing jada_google.py tool chain expected a Mac-side OAuth redemption, not a phone-mediated flow.
Technical Architecture: PKCE + Copy/Paste Redemption
Solution: Split the OAuth handshake so the authorization code travels back via manual copy/paste instead of HTTP redirect. Built `/Users/cb/jada-icm/tools/phone_oauth.py` to generate and redeem phone-friendly OAuth links.
Generation Phase
- PKCE (Proof Key for Public Clients): Generate a 43-character code verifier, hash it to a code challenge, and include both in the auth request. Scopes:
gmail.readonly,gmail.send,gmail.modify,calendar. - State parameter: Cryptographically random 32-byte state token, persisted to
/Users/cb/icloud-jada-ops/.pending-phone-oauth-statefor validation on redemption. - Auth URL: Encodes
response_type=code,redirect_uri=http://localhost,code_challenge_method=S256, and full scope list. User opens link on phone.
Phone-Side Flow
- User signs in to
jadasailing@gmail.comand approves all requested scopes. - Browser redirects to
http://localhost/?state=...&code=4/0Ad...— "can't connect" page appears (expected). - User taps address bar, selects all, copies the full localhost URL.
- User pastes URL back to Claude, which extracts state and code.
Redemption Phase
- URL parsing: Three paste shapes handled: bare
localhost/?state=...&code=...,http://localhost..., and full URLs with trailing content. - State validation: Compare parsed state against persisted pending state. Abort if mismatch (prevents CSRF replay).
- Code exchange: POST to
https://oauth2.googleapis.com/tokenwith code, verifier, and client credentials. Receive access token and refresh token. - Token storage: Write refresh token + access token metadata to
/Users/cb/jada-secrets/jada-token.json. - Mint stamp: Write timestamp to
/Users/cb/jada-secrets/jada-token-minted.txt(used by nightly refresh logic to distinguish fresh mints from routine token refreshes). - Cleanup: Delete pending-state file to mark flow complete.
Infrastructure & Integration
OAuth App Configuration
Existing JADA OAuth app (client ID 816564630881-rppcgpu27f9sk5cgnd2vik4686bg6odi.apps.googleusercontent.com) is published to Production status in Google Cloud Console, not Testing. This ensures refresh tokens don't expire after 7 days — critical for 24/7 automation (email dispatch, calendar sync, crew scheduling).
Token Lifecycle Management
Nightly test suite runs /Users/cb/icloud-repos/sites/queenofsandiego.com/tests/test_google_token.py (added this session) via tests-run.sh wrapper. Test checks token expiry and auto-refreshes if within 1 hour of expiration, writing updated access token back to jada-token.json. Mint stamp persists unchanged across refreshes — only a new redemption via phone_oauth.py rewrites the mint stamp, signaling a fresh grant cycle.
Routing & Health Checks
Updated /Users/cb/icloud-jada-ops/CONTEXT.md router to include phone-oauth flow entry. Health check command tools/jada_google.py --health reports current token state and mints a fresh auth link on demand (used during dev/test phases). Command runs from Lightsail only — Mac-side tools read tokens from iCloud bridge symlinks (~/icloud-jada-ops).
Key Decisions & Tradeoffs
Why PKCE?
Standard OAuth with just client ID and secret assumes a confidential client (server-side). Our phone flow is a public client scenario — the code is visible in the browser URL and could be intercepted. PKCE adds a cryptographic proof: only the entity that generated the original code challenge can redeem the code. Defends against code interception even if someone copies the redirect URL.
Why Copy/Paste, Not a Custom Redirect Handler?
- Mobile constraint: iOS/Android don't allow custom
http://URI schemes in non-native apps — bypassing this requires jailbreak. - Simplicity: Copy/paste requires zero infrastructure on the phone side. User perceives it as "paste the URL, done."
- Auditability: Manual paste creates a clear audit trail (code visible in chat transcript, state logged in FIRES ledger).
Why Production OAuth App Status Matters
Google's Testing status grants 7-day refresh token TTL — fine for one-off testing, but unacceptable for always-on services. Promoting to Production status increases TTL to 6 months (or indefinite, depending on the grant) and is required for any multi-service integration. This session published the OAuth app to Production, unblocking the permanent token workflow.
Testing & Validation
- Unit test:
test_google_token.pyvalidates token read/refresh logic in isolation (mocks Google endpoint). - Integration test: Nightly suite runs actual refresh against Google's API — catches credential changes, API updates, scope drift.
- FIRES ledger: Each redemption attempt (successful or failed) logged as a separate incident row in
/Users/cb/icloud-jada-ops/FIRES.mdwith state token, code, error (if any), and remediation. Enables post-incident forensics and trend analysis.
Decision Documentation
Created /Users/cb/icloud-jada-ops/decisions/phone-oauth-flow.md (1-page decision doc per ICM governance). Covers problem statement, rationale for PKCE + copy/paste, security assumptions, token lifecycle, and integration points. Reviewed and referenced by any future changes to Google auth.
What's Next
- Code redemption: User opens phone-friendly auth link, approves, copies redirect URL from Safari's address bar, pastes back within ~10 minutes. Claude redeems code and mints permanent token.
- Verification: Health check confirms mint timestamp, validates refresh token TTL, and reports to Slack/SMS on success.
- Automation unfreezing: Once token is minted, nightly test suite begins routine refresh cycles. Email dispatch, calendar sync, and crew page automation resume normal operation.
- Future phone OAuth flows: Pattern is now reusable for other services (e.g., Stripe, GitHub) that require phone-based grant flows.
phone_oauth.pygeneralizes to accept arbitrary OAuth endpoints and scopes.