```html

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-state for 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.com and 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/token with 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.py validates 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.md with 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.py generalizes to accept arbitrary OAuth endpoints and scopes.
```