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.mdrouter, 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.mdrow links OAuth scopes and test paths - Decision doc:
/Users/cb/icloud-jada-ops/decisions/phone-oauth-flow.mdrecords why PKCE + copy/paste was chosen over re-inventing localhost - Incident tracking:
FIRES.mdledger captures the original network flake and retry hardening as preventative measures - CLI tool:
jada_google.py --healthcan 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.pyimports the token and callsgmail().users().getProfile(userId='me')— passes iff the token is valid and non-expired - Manual health check:
jada_google.py --healthperforms 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