{"record":{"id":"9101f9f762263509","repo":"zeroclaw-labs/zeroclaw","slug":"oauth-state-mismatch-9101f9","errorCode":null,"errorMessage":"OAuth state mismatch","messagePattern":"OAuth state mismatch","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-providers/src/auth/openai_oauth.rs","lineNumber":322,"sourceCode":"\n    let params = parse_query_params(query);\n    let is_callback_payload = trimmed.contains('?')\n        || params.contains_key(\"code\")\n        || params.contains_key(\"state\")\n        || params.contains_key(\"error\");\n\n    if let Some(err) = params.get(\"error\") {\n        let desc = params\n            .get(\"error_description\")\n            .cloned()\n            .unwrap_or_else(|| \"OAuth authorization failed\".to_string());\n        anyhow::bail!(\"OpenAI OAuth error: {err} ({desc})\");\n    }\n\n    if let Some(expected_state) = expected_state {\n        if let Some(got) = params.get(\"state\") {\n            if got != expected_state {\n                anyhow::bail!(\"OAuth state mismatch\");\n            }\n        } else if is_callback_payload {\n            anyhow::bail!(\"Missing OAuth state in callback\");\n        }\n    }\n\n    if let Some(code) = params.get(\"code\").cloned() {\n        return Ok(code);\n    }\n\n    if !is_callback_payload {\n        return Ok(trimmed.to_string());\n    }\n\n    anyhow::bail!(\"Missing OAuth code in callback\")\n}\n\npub fn extract_account_id_from_jwt(token: &str) -> Option<String> {","sourceCodeStart":304,"sourceCodeEnd":340,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-providers/src/auth/openai_oauth.rs#L304-L340","documentation":"Thrown by `parse_code_from_redirect` when an OpenAI OAuth callback carries a `state` query parameter that differs from the `expected_state` the caller passed. The state parameter is the CSRF defense of the authorization-code flow: it ties the redirect back to the authorize request that started it. A mismatch means the incoming callback belongs to a different or stale login attempt, so the code is rejected before it can be exchanged.","triggerScenarios":"Calling `parse_code_from_redirect(input, Some(expected))` directly, or via `receive_loopback_code` on 127.0.0.1:1455, where the callback query has `state=<other>` and `<other> != expected` — e.g. input `/auth/callback?code=x&state=a` when the flow started with state `b`. The state check only runs when an expected_state is supplied.","commonSituations":"Two login flows racing on the same fixed loopback port (an old browser tab's redirect lands after a new flow started); reusing an authorize URL built from an earlier run with a fresh PkceState; re-running the login command while a previous callback is still in flight.","solutions":["Restart the login flow so `build_authorize_url` and `parse_code_from_redirect` share one fresh `PkceState` (new state + PKCE verifier)","Close stale browser tabs on auth.openai.com or localhost:1455 before retrying","Run only one login attempt at a time per machine — the loopback listener is a fixed port (1455)","If you call the parser directly, pass the exact `pkce.state` you embedded in the authorize URL"],"exampleFix":"// before: URL built with an old PkceState, callback checked against a new one\nlet url = build_authorize_url(&old_pkce);\n// later, in the callback handler:\nlet code = parse_code_from_redirect(path, Some(&new_pkce.state))?; // \"OAuth state mismatch\"\n\n// after: one PkceState drives the whole flow\nlet pkce = generate_pkce_state();\nlet url = build_authorize_url(&pkce);\n// later:\nlet code = parse_code_from_redirect(path, Some(&pkce.state))?;","handlingStrategy":"validation","validationCode":"fn state_matches(input: &str, expected: &str) -> bool {\n    input.split_once('?').map_or(false, |(_, q)| {\n        q.split('&').any(|pair| pair == format!(\"state={}\", expected))\n    })\n}\n\n// only hand the path to the parser when the state round-trips\nif state_matches(path, &pkce.state) {\n    let code = parse_code_from_redirect(path, Some(&pkce.state))?;\n}","typeGuard":null,"tryCatchPattern":"match parse_code_from_redirect(path, Some(&pkce.state)) {\n    Ok(code) => exchange(code),\n    Err(e) if e.to_string().contains(\"state mismatch\") => restart_login_flow().await, // stale attempt: discard\n    Err(e) => return Err(e),\n}","preventionTips":["Thread one PkceState through build_authorize_url, receive_loopback_code, and exchange_code_for_tokens","Serialize logins behind a mutex or CLI lock so two flows never share loopback port 1455","Log mismatched callbacks and never replay them — treat them as hostile input"],"tags":["oauth","openai","csrf","state","loopback","rust"],"backgroundTag":"oauth-state-mismatch","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}