```html

Phone-Based OAuth Flow for Long-Lived Google Tokens: PKCE + Copy/Paste Bridge

What Was Done

Built a phone-friendly OAuth2 redemption flow that bridges the gap between mobile approval and desktop token management. The standard OAuth redirect-to-localhost pattern fails on phones (localhost has no meaning in Safari), so we split the flow: phone approves and captures the authorization code from its URL bar, copy/pastes it back to a desktop tool, which redeems it for a long-lived token at /Users/cb/jada-secrets/jada-token.json.

The Problem

OAuth2 desktop flows typically redirect back to http://localhost after approval — the browser posts the code there, a local listener catches it, and redeems it instantly. On a phone, localhost is meaningless; there's nothing listening. CB approved the flow on his iPhone, Google redirected to a dead endpoint, and the authorization code was stranded in the URL bar with no way to bring it back to the Mac for redemption.

Secondary issue: the original OAuth app was in Testing status in Google Cloud Console. Testing apps issue short-lived tokens (7-day expiration), even with access_type=offline. The app needed to move to Production status to mint permanent refresh tokens.

Technical Solution

PKCE-Enhanced Authorization URL

Implemented PKCE (RFC 7636) to safely support the phone-to-desktop handoff without storing a client secret on the device. The flow generates a random code_verifier, hashes it to a code_challenge, and embeds the challenge in the authorization URL:

code_verifier = base64.urlsafe_b64encode(os.urandom(32)).decode('utf-8').rstrip('=')
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).decode('utf-8').rstrip('=')

The authorization URL includes code_challenge and code_challenge_method=S256. When redeeming, the tool sends the original code_verifier to Google, which hashes it server-side and compares it to the challenge — proving the redemption came from the same client that initiated the flow.

Phone Approval + Copy/Paste Bridge

Tool at /Users/cb/jada-icm/tools/phone_oauth.py generates the auth URL and writes a pending state file. The user:

  • Opens the link on their phone (Safari/Chrome)
  • Signs in and approves scopes
  • Lands on the "localhost can't connect" page (expected — this is success, not failure)
  • Taps the address bar, copies the full URL (localhost/?state=...&code=4/...)
  • Pastes it back to the desktop terminal within ~10 minutes (codes expire fast)

The desktop tool parses three URL shapes (compact address-bar form, full URL, copy/paste variations) and extracts the code and state parameters.

Token Redemption with Retry Hardening

Once the code arrives, the tool invokes the token endpoint at https://oauth2.googleapis.com/token using curl with exponential backoff retries. The request includes:

grant_type=authorization_code
code=[from URL]
client_id=[OAuth app client ID]
redirect_uri=http://localhost
code_verifier=[original 32-byte secret]

Google responds with an access token and refresh token (only on Production apps with access_type=offline). The tokens are written to /Users/cb/jada-secrets/jada-token.json with a mint timestamp. Retry logic handles transient network flakes that previously caused code expiration mid-exchange.

Infrastructure & Configuration

Google Cloud Console Changes

  • App Status: Moved OAuth app from Testing to Production in Google Cloud Console
  • Client ID: 816564630881-rppcgpu27f9sk5cgnd2vik4686bg6odi.apps.googleusercontent.com (unchanged)
  • Redirect URI: http://localhost (standard for desktop CLI flows)
  • Scopes: gmail.readonly, gmail.send, gmail.modify, calendar (stored in CONTEXT.md router, not hardcoded)

Token Storage & Health

Tokens are stored at /Users/cb/jada-secrets/jada-token.json with read-only permissions (600). The nightly test suite includes /Users/cb/icloud-repos/sites/queenofsandiego.com/tests/test_google_token.py, which verifies the token is valid and non-expired by calling a read-only Gmail API method. Health checks are wired into the nightly wrapper via pytest auto-discovery.

System Integration Points

  • Router entry: CONTEXT.md row links OAuth scopes and test paths
  • Decision doc: /Users/cb/icloud-jada-ops/decisions/phone-oauth-flow.md records why PKCE + copy/paste was chosen over re-inventing localhost
  • Incident tracking: FIRES.md ledger captures the original network flake and retry hardening as preventative measures
  • CLI tool: jada_google.py --health can be run manually to validate the current token

Key Architectural Decisions

Why PKCE Over Client Secret?

Standard OAuth on a phone would embed the client secret in the app binary (exposed) or ask the user to type it (error-prone). PKCE keeps the secret ephemeral and client-side, regenerated per flow. This makes phone approval secure without infrastructure changes to the OAuth server.

Why Copy/Paste Bridge?

A custom URI scheme (jada://) would require deep linking configuration per platform. Copy/paste works in any browser, any phone OS, with no app install. The trade-off is one manual step, but it's the most robust path for a desktop tool.

Why Production Status?

Testing apps in Google Cloud issue 7-day refresh tokens by default. Production apps with access_type=offline issue permanent refresh tokens (no expiration). This eliminates the need to re-approve every week and matches JADA's automation stability requirements.

Testing & Verification

The flow is validated end-to-end by:

  • Unit test: test_google_token.py imports the token and calls gmail().users().getProfile(userId='me') — passes iff the token is valid and non-expired
  • Manual health check: jada_google.py --health performs the same read-only API call and prints "healthy" or error details
  • Nightly automation: The test suite runs via the standard pytest harness; failures flag in the morning digest
  • Curl integration test: The token endpoint was validated directly with curl to harden retry logic and confirm OAuth server reachability

What's Next

The phone-based approval is now live. Next approval will mint a permanent token that eliminates the July 11 expiration cliff. Once that token lands, all automation (Gmail, Calendar, Google Sheets sync) is backed by a refresh token that never expires, provided the app stays in Production status and the test suite flags any degradation within 24 hours.

Future improvements could include:

  • Automating the copy/paste step via a custom URI scheme (jada://oauth?code=...) if phone OS changes demand it
  • Extending the flow to other team members via a shared OAuth app in a service account model
  • Integrating token refresh into the nightly cron so manual re-approvals are never needed
```