{"record":{"id":"bb9e85dc80412922","repo":"tinyhumansai/openhuman","slug":"openrouter-oauth-callback-state-did-not-match-the","errorCode":null,"errorMessage":"OpenRouter OAuth callback state did not match the request.","messagePattern":"OpenRouter OAuth callback state did not match the request\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/utils/openrouterOAuth.ts","lineNumber":55,"sourceCode":"  return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/g, '');\n}\n\nasync function createCodeChallenge(verifier: string): Promise<string> {\n  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));\n  return base64UrlEncode(new Uint8Array(digest));\n}\n\nfunction extractOAuthCode(callbackUrl: string, expectedState: string): string {\n  let parsed: URL;\n  try {\n    parsed = new URL(callbackUrl);\n  } catch {\n    throw new Error('OpenRouter OAuth returned an invalid callback URL.');\n  }\n\n  const actualState = parsed.searchParams.get('state');\n  if (actualState !== expectedState) {\n    throw new Error('OpenRouter OAuth callback state did not match the request.');\n  }\n\n  const code = parsed.searchParams.get('code');\n  if (!code) {\n    throw new Error('OpenRouter OAuth did not return an authorization code.');\n  }\n  return code;\n}\n\nasync function exchangeCodeForKey(\n  code: string,\n  verifier: string,\n  fetchImpl: typeof fetch\n): Promise<string> {\n  const response = await fetchImpl(OPENROUTER_TOKEN_URL, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ code, code_verifier: verifier, code_challenge_method: PKCE_METHOD }),","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/utils/openrouterOAuth.ts#L37-L73","documentation":"Standard OAuth 2.0 CSRF protection: the flow starts with a random state, the provider echoes it on redirect, and extractOAuthCode() compares parsed.searchParams.get('state') against expectedState. A mismatch means the callback belongs to a different authorization attempt than the one this client is currently finishing.","triggerScenarios":"Two OAuth attempts sharing loopback port 3000 (a stale browser tab completing an old flow while a new one waits); app restart between authorize and callback so expectedState was regenerated/lost; a forged or replayed callback hitting the listener.","commonSituations":"User re-clicks 'Connect OpenRouter' in a second tab; listener from a previous session still bound; expected state kept in a store that was cleared on reload.","solutions":["Serialize the flow: one OAuth attempt at a time, cancel/close previous tabs and listeners before starting a new one","Keep expectedState in memory tied to the listener handle so a restart creates a fresh pair instead of matching stale callbacks","On mismatch, abort and restart the whole flow (new verifier + state) rather than retrying with the old pair"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Serialize flows so only one state is ever outstanding:\nlet inflight: AbortController | null = null;\nfunction beginFlow() {\n  inflight?.abort();\n  inflight = new AbortController();\n  return inflight.signal;\n}","typeGuard":null,"tryCatchPattern":"try { const code = extractOAuthCode(callbackUrl, state); }\ncatch (err) {\n  if (err instanceof Error && err.message.includes('state did not match')) {\n    // stale/foreign callback — ignore it, keep waiting for the current state,\n    // or abort and restart the flow with fresh verifier+state\n  } else throw err;\n}","preventionTips":["One authorization attempt at a time; abort the previous listener before starting a new one","Bind expectedState to the listener handle's lifetime, not to a store that survives reloads","Treat state mismatch as possible CSRF: never proceed with a mismatched callback"],"tags":["oauth","openrouter","csrf","state-mismatch","security"],"backgroundTag":"oauth-state-mismatch","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}