```html

Phone-Based OAuth 2.0 Token Redemption Without Localhost Redirect

The Problem

Standard OAuth 2.0 redirect flow assumes a desktop browser can listen on localhost:8080 or similar. When authenticating from a phone to control backend systems running on a separate machine (in this case, a Mac managing JADA automation systems), Google's Authorization Server redirects to http://localhost — which on a phone resolves to "the phone itself," not the remote system. The authorization code sits buried in the phone's address bar, unreachable by the backend.

This is a known constraint, not a bug: RFC 6749 (OAuth 2.0) Section 4.1.2 assumes the user-agent and the token-redeeming system occupy the same network namespace. Cross-device approval requires either:

  • A public callback URL (exposes credentials to network)
  • Device-pairing code flow (requires separate approval mechanism)
  • Manual code copy-paste (user-hostile but simple)

We chose option 3 with hardened transport and state validation.

Solution Architecture

The fix splits OAuth approval from token redemption into two phases:

Phase 1 (Phone): User approves on their device, lands on "can't connect" error page, copies the full redirect URL from the address bar.

Phase 2 (Backend): Backend receives the code via paste, redeems it via a server-to-server HTTPS POST to Google's token endpoint, validates the state parameter, and stores the token securely.

Implementation lives in /Users/cb/jada-icm/tools/phone_oauth.py, which handles:

  • PKCE support: Implements Proof Key for Public Clients (RFC 7636), generating code_challenge and code_verifier pairs. This prevents authorization code interception attacks, especially important when codes travel over user-copy channels.
  • State generation: Cryptographically secure random 24-byte state, base64url-encoded, stored in a pending registry file.
  • URL parsing: Handles three paste formats: full redirect URLs, compact localhost shorthand, and partial query strings.
  • Retry logic: Curl-based token redemption with exponential backoff (3 attempts, 2-second base interval) to survive transient network drops.

Technical Implementation

Generating the Approval Link:

python3 /Users/cb/jada-icm/tools/phone_oauth.py --generate \
  --client-id 816564630881-rppcgpu27f9sk5cgnd2vik4686bg6odi.apps.googleusercontent.com \
  --scopes gmail.readonly,gmail.send,gmail.modify,calendar \
  --output-state ~/jada-ops/pending-oauth.txt

Outputs a full authorization URL with embedded state, code_challenge, code_challenge_method=S256, access_type=offline, and prompt=consent. The state and verifier pair are written to pending-oauth.txt with a 10-minute expiry timestamp.

Redeeming the Code:

python3 /Users/cb/jada-icm/tools/phone_oauth.py --redeem \
  "localhost/?state=bdtfdyAtYOYQ1wifFcN2vTFoj1vFQY&code=4/0AdkVLPzs4LlpFCxHQebyb6XpPcJTO8DG8kt1xKIOqeldRQLTN1Dr9um0IDZBcbSUBgC9Rw" \
  --output-token ~/jada-secrets/jada-token.json

The script:

  • Extracts state and code from the pasted URL (handles all three format variants)
  • Validates state against the pending registry
  • Looks up the corresponding code_verifier
  • POSTs to Google's token endpoint (https://oauth2.googleapis.com/token) with verifier and code
  • On success, writes the token response to /Users/cb/jada-secrets/jada-token.json and logs a timestamp ("mint stamp") for expiry tracking
  • On failure (network error, invalid code, state mismatch), retries up to 3 times before surfacing the error

Token Lifecycle and Production Publishing

A critical discovery during this session: Google OAuth app status affects token lifetime.

  • Testing status: Tokens expire after 7 days, regardless of offline access request
  • Production status: Tokens expire after ~3 months (and refresh tokens are permanent until revoked)

The JADA OAuth app was initially in Testing, which meant every token minted during testing self-destructed around July 11, 2026. Moving to Production (via Google API console, marking the app as verified) ensures durable tokens that survive redeployments and maintenance windows.

The jada_google.py health-check tool (invoked by CI/CD) now validates:

tools/jada_google.py --health

This:

  • Reads the token from ~/jada-secrets/jada-token.json
  • Calls https://www.googleapis.com/oauth2/v1/tokeninfo to confirm validity
  • Checks scopes against the required set (gmail, calendar)
  • Extracts and logs the mint timestamp to compare against expected app status (Testing vs Production)
  • Flags expiry warnings if the token is within 3 days of invalidation

Infrastructure and Routing

Token Storage: /Users/cb/jada-secrets/jada-token.json is a local file on the Mac (canonical credential home per [[environments-and-auth.md]]). This keeps API calls (Gmail, Calendar, Drive, Analytics) on the same machine; there is no cross-network credential exposure.

CONTEXT.md Router Entry: Added a new L2 route in /Users/cb/icloud-jada-ops/CONTEXT.md:

| phone-oauth | CB OAuth approval from phone → backend token redemption | tools/phone_oauth.py | pending-oauth.txt + jada-secrets/jada-token.json |

This ensures future team members (or future-me) know where the flow lives and what files are involved.

Test Coverage: Added /Users/cb/icloud-repos/sites/queenofsandiego.com/tests/test_google_token.py, which the nightly CI wrapper invokes to validate the current token is live and held the expected scopes. On failure, it triggers an SMS to CB with the health-check output.

Incident Tracking and Decision Documentation

This session produced two artifacts for future reference:

  • Decision Doc: /Users/cb/icloud-jada-ops/decisions/phone-oauth-flow.md documents the cross-device OAuth problem, why localhost redirect fails, and why we chose manual code-pasting + PKCE over device-pairing or public callback.
  • FIRES Ledger Entry: /Users/cb/icloud-jada-ops/FIRES.md now includes the transient network drop that ate the code redemption attempt, the 7-day Testing token expiry discovery, and the Production publish recovery. This ledger is the source of truth for all infrastructure incidents and anomalies, enabling the nightly diff-review bot (running on Lightsail) to flag recurring failure modes.

Why This Matters

This pattern unlocks secure multi-device OAuth without exposing credentials to third-party callback servers. It's especially valuable for:

  • Self-hosted systems: No public domain required; all credential exchange happens server-to-server over HTTPS
  • Headless approval: On-call staff can approve new tokens from anywhere (phone, tablet, untrusted laptop)
  • Offline-first flows: Copy-paste is human-readable, auditable, and doesn't require additional auth infrastructure

The retry logic and PKCE prove essential when the on-call Mac's network is unstable (common on cellular backup or VPN flap) — a single network hiccup no longer burns the authorization code or forces a full re-approval cycle.

What's Next

Monitor the nightly test_google_token.py` runs to confirm Production tokens are stable. The memory entry [[jada-google-auth-robust.md]] has been updated to reflect the new phone_oauth flow and the critical app-status-to-token-lifetime relationship. Future OAuth integrations (e.g., Google Drive automation) can reuse this same foundation.

```