Files
flynn/docs/plans/2026-02-26-anthropic-oauth-browser-flow-design.md

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

  1. Generate PKCE code_verifier (32 random bytes, base64url-encoded) and code_challenge (SHA256 of verifier, base64url-encoded).
  2. Generate random state (16 bytes, hex) for CSRF protection.
  3. 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.
  4. Build the authorization URL with all required params and call onOpen(url).
  5. onOpen opens the browser using child_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.
  6. Wait for the browser to redirect to http://127.0.0.1:{port}/callback?code=...&state=.... Server validates state, responds with an HTML "Authentication complete — you can close this tab." page, then closes.
  7. Exchange code + code_verifier at the token URL via POST.
  8. Store the returned access_token as the auth_token field 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 digest
  • loginAnthropicOAuth() with mocked http.createServer + mocked fetch:
    • 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 storeAnthropicAuthToken called with returned access_token
  • 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

  • --browser flag triggers loginAnthropicOAuth (mock it)
  • --browser with existing token: prompts for confirmation before re-authenticating
  • Existing --token and default API key tests unchanged

src/frontends/tui/minimal.test.ts

  • Anthropic option 2: mock loginAnthropicOAuth, verify it's called
  • credentialStored flag: 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