6.7 KiB
Anthropic OAuth Browser Flow — Design
Goal: Replace the manual auth-token paste in /login anthropic (option 2) and flynn anthropic-auth --token with a PKCE OAuth2 browser flow that obtains the token automatically via a local HTTP callback server.
Date: 2026-02-26
OAuth Endpoints
| Field | Value |
|---|---|
| Authorization URL | https://claude.ai/oauth/authorize |
| Token URL | https://console.anthropic.com/v1/oauth/token |
| Client ID | 9d1c250a-e61b-44d9-88ed-5944d1962f5e |
| Scopes | user:inference user:profile |
| PKCE method | S256 (SHA256) |
Note: These endpoints and the client ID belong to Claude Code CLI. Anthropic has restricted third-party use of Pro subscription tokens as of early 2026; the flow may only complete for users with an active Claude Pro/Max session in the browser.
Architecture
Flow
- Generate PKCE
code_verifier(32 random bytes, base64url-encoded) andcode_challenge(SHA256 of verifier, base64url-encoded). - Generate random
state(16 bytes, hex) for CSRF protection. - Bind a one-shot HTTP server to
127.0.0.1:0(OS picks a free port). Read the actual port from the server address. - Build the authorization URL with all required params and call
onOpen(url). onOpenopens the browser usingchild_process+ platform command (xdg-open/open/start). If launch fails, print the URL and instruct the user to open it manually — the server keeps running.- Wait for the browser to redirect to
http://127.0.0.1:{port}/callback?code=...&state=.... Server validatesstate, responds with an HTML "Authentication complete — you can close this tab." page, then closes. - Exchange
code+code_verifierat the token URL via POST. - Store the returned
access_tokenas theauth_tokenfield in~/.config/flynn/auth.json.
Timeout: 5 minutes. If the server receives no callback, it closes and throws Error('Anthropic OAuth timed out after 5 minutes.').
Components
src/auth/anthropic.ts — new additions
// PKCE
function generateCodeVerifier(): string
// crypto.randomBytes(32).toString('base64url')
function generateCodeChallenge(verifier: string): string
// createHash('sha256').update(verifier).digest('base64url')
// One-shot callback server
// Resolves with { code, state } when redirect arrives, rejects on timeout
function startCallbackServer(port: number, timeoutMs: number): Promise<{ code: string; state: string }>
// Token exchange
async function exchangeCodeForToken(
code: string,
codeVerifier: string,
redirectUri: string,
): Promise<string> // returns access_token
// Full public flow
export async function loginAnthropicOAuth(
onOpen: (url: string) => void,
): Promise<string>
// Orchestrates all steps, calls storeAnthropicAuthToken() before returning
src/frontends/tui/minimal.ts — Anthropic branch change
Option 2 ("Paste auth token") becomes "Browser OAuth":
Anthropic login:
1) Paste API key
2) Browser OAuth (Claude Pro/Max)
Option 2 flow:
console.log(`${colors.gray}Starting Anthropic browser OAuth...${colors.reset}`);
await loginAnthropicOAuth((url) => {
openBrowser(url); // platform child_process call
console.log(`${colors.gray}Opening browser. If it didn't open, visit:${colors.reset}`);
console.log(url);
console.log(`${colors.gray}Waiting for authentication...${colors.reset}`);
});
console.log(`${colors.gray}Anthropic auth token stored.${colors.reset}\n`);
The credentialStored flag pattern (already present) applies here too.
src/cli/anthropic-auth.ts — new --browser flag
Add alongside the existing --token flag:
--browser Obtain auth token via browser OAuth flow (Claude Pro/Max)
if (mode === 'browser') {
console.log('Starting Anthropic browser OAuth...');
await loginAnthropicOAuth((url) => {
openBrowser(url);
console.log(`If browser didn't open, visit:\n${url}`);
console.log('Waiting for authentication...');
});
console.log('Anthropic auth token stored in ~/.config/flynn/auth.json');
}
parseAnthropicAuthMode gains a third value: 'api' | 'token' | 'browser'.
Browser opening helper (inline, no new dependency)
function openBrowser(url: string): void {
const cmd = process.platform === 'win32' ? 'start'
: process.platform === 'darwin' ? 'open'
: 'xdg-open';
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
}
Error Handling
| Scenario | Behaviour |
|---|---|
| Port already in use | 127.0.0.1:0 avoids this — OS picks free port |
| Browser open fails | Print URL, keep server running, user opens manually |
| State mismatch | Throw Error('OAuth state mismatch — possible CSRF attack.') |
| Timeout (5 min) | Throw Error('Anthropic OAuth timed out after 5 minutes.') |
| Token exchange non-2xx | Throw Error('Token exchange failed (${status}): ${body}') |
| 403 from Anthropic | Surface message: "Anthropic OAuth requires an active Claude Pro/Max subscription." |
Testing
src/auth/anthropic.test.ts — new tests
generateCodeVerifier()returns 43-char base64url string (no padding, URL-safe chars only)generateCodeChallenge(verifier)returns correct SHA256 base64url digestloginAnthropicOAuth()with mockedhttp.createServer+ mockedfetch:- Verifies auth URL contains correct
client_id,scope,code_challenge_method=S256,redirect_uri,state - Verifies token exchange POST body contains
code,code_verifier,redirect_uri - Verifies
storeAnthropicAuthTokencalled with returnedaccess_token
- Verifies auth URL contains correct
- State mismatch → throws
- Timeout (use fake timers) → throws
- Token exchange 403 → throws with subscription message
- Token exchange other non-2xx → throws with status + body
src/cli/anthropic-auth.test.ts — new tests
--browserflag triggersloginAnthropicOAuth(mock it)--browserwith existing token: prompts for confirmation before re-authenticating- Existing
--tokenand default API key tests unchanged
src/frontends/tui/minimal.test.ts
- Anthropic option 2: mock
loginAnthropicOAuth, verify it's called credentialStoredflag: verify auth_mode prompt only appears after successful OAuth
Files Changed
| File | Change |
|---|---|
src/auth/anthropic.ts |
Add PKCE helpers, callback server, token exchange, loginAnthropicOAuth |
src/auth/anthropic.test.ts |
New tests for all new functions |
src/frontends/tui/minimal.ts |
Replace option 2 paste with browser OAuth |
src/frontends/tui/minimal.test.ts |
Update/add Anthropic login tests |
src/cli/anthropic-auth.ts |
Add --browser flag, browser auth mode |
src/cli/anthropic-auth.test.ts |
Tests for --browser flag |