{"record":{"id":"bc11a870222b80ea","repo":"decolua/9router","slug":"windsurf-callback-state-mismatch","errorCode":null,"errorMessage":"Windsurf callback state mismatch","messagePattern":"Windsurf callback state mismatch","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"src/lib/oauth/providers/windsurf.js","lineNumber":44,"sourceCode":"  const text = String(raw || \"\").trim();\n  let queryStr = text;\n  if (text.includes(\"?\")) queryStr = text.slice(text.indexOf(\"?\") + 1);\n  if (text.startsWith(\"#\")) queryStr = text.slice(1);\n  const params = Object.fromEntries(new URLSearchParams(queryStr));\n  const pick = (keys) => {\n    for (const k of keys) { const v = params[k]; if (v && String(v).trim()) return String(v).trim(); }\n    return null;\n  };\n  const err = pick([\"error\"]);\n  if (err) {\n    const desc = pick([\"error_description\"]);\n    throw new Error(desc ? `Windsurf auth failed: ${err} (${desc})` : `Windsurf auth failed: ${err}`);\n  }\n  const accessToken = pick([\"access_token\", \"token\"]);\n  if (!accessToken) throw new Error(\"Windsurf callback missing access_token\");\n  const state = pick([\"state\"]);\n  if (expectedState && state && state !== expectedState) {\n    throw new Error(\"Windsurf callback state mismatch\");\n  }\n  return { firebaseIdToken: accessToken };\n}\n\n// POST RegisterUser {firebase_id_token} → {apiKey, apiServerUrl, name}\nasync function fetchWindsurfRegisterUser(firebaseIdToken) {\n  const data = await windsurfSeatRequest(WINDSURF_CONFIG.registerApiBaseUrl, WINDSURF_CONFIG.registerPath, {\n    firebase_id_token: firebaseIdToken,\n  });\n  const apiKey = extractJsonPath(data, [[\"apiKey\"], [\"api_key\"]]);\n  if (!apiKey) throw new Error(\"Windsurf RegisterUser missing apiKey\");\n  const apiServerUrl = extractJsonPath(data, [[\"apiServerUrl\"], [\"api_server_url\"]]) || WINDSURF_CONFIG.defaultApiServerUrl;\n  const name = extractJsonPath(data, [[\"name\"]]);\n  return { apiKey, apiServerUrl, name };\n}\n\n// Best-effort: GetOneTimeAuthToken → GetCurrentUser → email/name.\nasync function fetchWindsurfUserInfo(apiServerUrl, firebaseIdToken) {","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/providers/windsurf.js#L26-L62","documentation":"parseWindsurfCallback throws this when the callback carries a state parameter that does not equal the expectedState generated at flow start. State is CSRF protection: a mismatch means the callback may not belong to the session that initiated the OAuth flow. The library only enforces it when expectedState was supplied and a state param is present.","triggerScenarios":"Two OAuth flows running concurrently (second callback delivered to the first's listener); user completing a stale auth URL from an earlier attempt; replaying an old callback URL; paste-token flow where state from a different session is pasted.","commonSituations":"Clicking 'connect' twice and finishing the older browser tab; re-running the flow and pasting a URL saved from the previous attempt; multiple accounts connected in parallel tabs.","solutions":["Restart the OAuth flow cleanly: open the newly generated auth URL and use only ITS redirect callback","Discard stale tabs / previous authorization URLs; never paste a URL from an earlier attempt","Ensure only one Windsurf connect flow is active at a time (single callback listener/port)","If state was genuinely lost (e.g. a proxy dropped it), verify the token via RegisterUser and treat the flow as unvalidated at your own risk"],"exampleFix":"// before: reusing an old callback URL from a previous session\nconst tokens = await windsurf.exchangeToken(cfg, oldCallbackUrl, redirectUri, null, currentState);\n// after: generate a fresh flow each time\nconst state = crypto.randomBytes(16).toString('hex');\nconst authUrl = windsurf.buildAuthUrl(cfg, redirectUri, state);\n// ... open authUrl, receive callback for THIS state ...\nconst tokens = await windsurf.exchangeToken(cfg, freshCallbackUrl, redirectUri, null, state);","handlingStrategy":"validation","validationCode":"// compare states yourself before invoking the parser for clearer UX\nconst stateFrom = (raw) => new URLSearchParams(String(raw).split('?')[1] || '').get('state');\nconst s = stateFrom(callbackUrl);\nif (expectedState && s && s !== expectedState) throw new Error('Stale callback — restart the connect flow (state mismatch)');","typeGuard":"const stateMatches = (raw, expected) => {\n  const s = new URLSearchParams(String(raw).split('?')[1] || '').get('state');\n  return !s || !expected || s === expected;\n};","tryCatchPattern":"try { ({ firebaseIdToken } = parseWindsurfCallback(raw, state)); }\ncatch (e) {\n  if (e.message === 'Windsurf callback state mismatch') {\n    return restartFlow('Callback from a previous attempt detected — starting a fresh sign-in');\n  }\n  throw e;\n}","preventionTips":["Generate a fresh random state per flow and store it until the callback arrives","Run only one OAuth flow at a time per callback port/listener","Never paste callback URLs from a previous session","Discard in-flight flows when the user re-clicks connect"],"tags":["oauth","csrf","state-mismatch","windsurf"],"backgroundTag":"oauth-state-mismatch","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}