{"record":{"id":"1ba2a262fee66197","repo":"ruvnet/ruflo","slug":"state-mismatch-the-oauth-callback-did-not-match","errorCode":null,"errorMessage":"state mismatch — the OAuth callback did not match the request this CLI sent","messagePattern":"state mismatch — the OAuth callback did not match the request this CLI sent","errorType":"exception","errorClass":"StateMismatchError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/auth/client.ts","lineNumber":132,"sourceCode":"  }\n}\n\n/** Browser-based loopback PKCE login — the ADR-306 default for an interactive desktop. */\nexport async function browserLogin(print: (line: string) => void): Promise<LoginResult> {\n  const sec = await loadSecurityOAuth();\n  const server = await sec.CallbackServer.bind();\n  const pkce = sec.generatePkce();\n  const url = sec.authorizeUrl(server.redirectUri, pkce.state, pkce.codeChallenge);\n\n  print('Opening your browser to sign in to Cognitum...');\n  print(`If it doesn't open automatically, visit:\\n\\n  ${url}\\n`);\n  await sec.openBrowser(url).catch(() => {}); // best-effort — the URL above is always the fallback\n  print('Waiting for you to finish signing in...');\n\n  const result = await server.awaitCallback();\n  const validated = validateCallback(result.error, result.code, result.state, pkce.state);\n  if (!validated.ok) {\n    if (validated.reason === 'state-mismatch') throw new StateMismatchError();\n    throw new LoginDeniedError(validated.detail ?? 'unknown');\n  }\n\n  const tokens = await sec.exchangeCode(validated.code, pkce.codeVerifier, server.redirectUri);\n  return { tokens, method: 'pkce' };\n}\n\n/** Headless fallback: prints the authorize URL with the OOB redirect, prompts for the pasted code. */\nexport async function manualLogin(\n  print: (line: string) => void,\n  input: NodeJS.ReadableStream = process.stdin,\n): Promise<LoginResult> {\n  const sec = await loadSecurityOAuth();\n  print('Browser-based callback unavailable (SSH/container detected, or --no-browser).\\n');\n\n  const pkce = sec.generatePkce();\n  const url = sec.authorizeUrl(sec.OOB_REDIRECT_URI, pkce.state, pkce.codeChallenge);\n  print(`Open this URL in a browser and authorize:\\n\\n  ${url}\\n`);","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/cli/src/auth/client.ts#L114-L150","documentation":"StateMismatchError is the OAuth CSRF guard: the loopback callback arrived with a state parameter that doesn't equal the random state generated for THIS login attempt (or missing entirely). It is thrown by browserLogin after validateCallback returns reason 'state-mismatch'. The check is order-sensitive — an error param or missing code would have thrown LoginDeniedError instead — so reaching this error means a code was presented but the state didn't match.","triggerScenarios":"browserLogin() callbacks hitting the wrong loopback server: two concurrent `ruflo auth login` runs sharing a port, a stale browser tab completing an older authorize request after a new one started, a proxy/extension rewriting callback query params, or the callback's state being dropped by a redirect chain.","commonSituations":"User re-runs login while the first browser tab is still open, then authorizes the old tab; port reuse across CLI invocations; corporate proxies or security software stripping query parameters from localhost callbacks; clock-skewed test harnesses replaying captured callback URLs.","solutions":["Close all stale authorize tabs and run a single fresh `ruflo auth login` — concurrent flows are the dominant cause","If it persists, check for proxies/extensions interfering with localhost:PORT callbacks (state must round-trip untouched)","Retry the login; a fresh flow generates a fresh state, so a one-off mismatch self-heals","For headless/CI environments use `--token-stdin` or the manual code flow instead of the browser loopback flow","If you're implementing your own callback handler, ensure it forwards state verbatim to server.awaitCallback"],"exampleFix":"// before — stale tab satisfies a new flow's server\n// (two logins racing on the same loopback port)\n\n// after — serialize logins and surface a clear retry\ntry {\n  const result = await browserLogin(print);\n} catch (e) {\n  if (e instanceof StateMismatchError) {\n    print('Stale login detected — close old browser tabs and re-run ruflo auth login.');\n    process.exitCode = 1;\n  } else throw e;\n}","handlingStrategy":"try-catch","validationCode":"// before awaiting the callback, pin the expected state and reject foreign ones:\nconst expected = pkce.state;\nconst result = await server.awaitCallback();\nif (result.state !== expected) throw new StateMismatchError(); // pre-check mirrors the library","typeGuard":null,"tryCatchPattern":"import { StateMismatchError } from './auth/client.js';\n\ntry { await browserLogin(print); }\ncatch (e) {\n  if (e instanceof StateMismatchError) {\n    // tell user to close stale tabs and re-run once; do NOT auto-retry in a loop\n  }\n  throw e;\n}","preventionTips":["Run one login flow at a time — concurrent loopback servers cause cross-talk","Close stale authorize tabs before starting a new login","Keep localhost callback URLs untouched by proxies and extensions"],"tags":["oauth","csrf","pkce","state-mismatch","auth"],"backgroundTag":"oauth-state-mismatch","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}