{"record":{"id":"de3d3b4dc21d4142","repo":"zed-industries/zed","slug":"oauth-state-mismatch-x-ai-subscribed","errorCode":null,"errorMessage":"OAuth state mismatch","messagePattern":"OAuth state mismatch","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/x_ai_subscribed/src/x_ai_subscribed.rs","lineNumber":820,"sourceCode":"            oauth_callback_server::OAuthCallbackServerConfig {\n                host: CALLBACK_HOST,\n                preferred_port: CALLBACK_PORT,\n                fallback_port: None,\n                path: CALLBACK_PATH,\n            },\n        )\n        .context(\"Failed to start OAuth callback server\")?;\n\n    let pkce = new_pkce_authorize_request(redirect_uri)?;\n    cx.update(|cx| cx.open_url(&pkce.authorize_url));\n\n    let callback = callback_rx\n        .await\n        .map_err(|_| anyhow!(\"OAuth callback was cancelled\"))?\n        .context(\"OAuth callback failed\")?;\n\n    if callback.state != pkce.state {\n        return Err(anyhow!(\"OAuth state mismatch\"));\n    }\n\n    let tokens = exchange_code(\n        &http_client,\n        &callback.code,\n        &pkce.verifier,\n        &pkce.redirect_uri,\n    )\n    .await\n    .context(\"Token exchange failed\")?;\n\n    let refresh_token = tokens\n        .refresh_token\n        .filter(|token| !token.is_empty())\n        .context(\"Token response did not include a refresh_token\")?;\n    let email = tokens\n        .id_token\n        .as_deref()","sourceCodeStart":802,"sourceCodeEnd":838,"githubUrl":"https://github.com/zed-industries/zed/blob/916fc2b8cb3a815cbef4a3b40e13081be72036b6/crates/x_ai_subscribed/src/x_ai_subscribed.rs#L802-L838","documentation":"Raised in do_oauth_flow when the `state` query parameter of the received OAuth callback does not equal the random state value generated for the PKCE authorize request. OAuth state is a CSRF protection binding the browser redirect to this specific flow; a mismatch means the redirect either belongs to a different/concurrent flow, was replayed, or was forged. The library refuses to exchange the authorization code in that case.","triggerScenarios":"The callback URL delivered to the local callback server carries a state parameter different from pkce.state — e.g. two sign-in flows ran concurrently and the browser hit the stale one, the user reloaded an old redirect URL, or a malicious page redirected to a forged callback.","commonSituations":"User opens the authorize URL twice (two tabs/windows) and completes the older flow; leftover browser tab from a previous session redirects to the port now owned by a new server instance; cross-site request forgery attempt against the localhost callback port.","solutions":["Restart sign_in cleanly, ensuring only one OAuth flow runs at a time (cancel any in-flight one first).","Close stale browser tabs from previous sign-in attempts before retrying.","Do not disable the check — it protects against CSRF; investigate how a foreign callback reached the local server instead.","Verify no other application or flow is bound to the same callback port (CALLBACK_PORT) that could deliver mismatched state."],"exampleFix":"// before\nlet creds = sign_in(&http_client, cx).await?;\n// after\n// ensure only one flow at a time\nif let Some(inflight) = pending_sign_in.take() { inflight.abort(); }\nlet creds = sign_in(&http_client, cx).await\n    .inspect_err(|e| if e.to_string().contains(\"state mismatch\") {\n        log::warn!(\"Stale or forged OAuth callback rejected; retry sign-in\");\n    })?;","handlingStrategy":"validation","validationCode":"// before exchanging the code, confirm single-flow ownership of the callback port\nlet server_bound = std::net::TcpListener::bind((CALLBACK_HOST, CALLBACK_PORT)).is_err();\nif !server_bound {\n    return Err(anyhow!(\"another OAuth flow may be listening; stale callbacks possible\"));\n}","typeGuard":"fn state_matches(callback_state: &str, pkce_state: &str) -> bool {\n    // constant-time comparison to avoid timing side channels\n    callback_state.len() == pkce_state.len()\n        && callback_state.bytes().zip(pkce_state.bytes()).fold(0u8, |acc, (a, b)| acc | (a ^ b)) == 0\n}","tryCatchPattern":"match sign_in(&http_client, cx).await {\n    Err(e) if e.to_string().contains(\"OAuth state mismatch\") => {\n        log::warn!(\"callback state mismatch (CSRF or stale tab); restarting flow\");\n        sign_in(&http_client, cx).await\n    }\n    other => other,\n}","preventionTips":["Never compare OAuth state with a plain == if exposing a helper; use constant-time comparison","Abort previous sign-in flows before starting a new one","Instruct users to close old sign-in tabs after failed attempts","Keep the random state per-flow and never persist it across retries"],"tags":["oauth","csrf","security","state-mismatch"],"backgroundTag":"oauth-state-mismatch","analyzedSha":"916fc2b8cb3a815cbef4a3b40e13081be72036b6","analyzedAt":"2026-09-19T19:09:50.599Z","contentChangedAt":"2026-09-19T19:09:50.599Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}