{"record":{"id":"23d4c406ec13402c","repo":"zeroclaw-labs/zeroclaw","slug":"missing-oauth-state-in-callback","errorCode":null,"errorMessage":"Missing OAuth state in callback","messagePattern":"Missing OAuth state in callback","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-providers/src/auth/openai_oauth.rs","lineNumber":325,"sourceCode":"        || 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> {\n    let payload = token.split('.').nth(1)?;\n    let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD\n        .decode(payload)","sourceCodeStart":307,"sourceCodeEnd":343,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-providers/src/auth/openai_oauth.rs#L307-L343","documentation":"Thrown when `parse_code_from_redirect` was given an `expected_state`, the input looks like a real callback (has `?`, or carries `code`/`state`/`error` params), but the query has no `state` parameter at all. The library refuses to process a callback that cannot prove it originated from your authorize request. Raw-code input without callback shape is still accepted; only callback-shaped payloads must carry state.","triggerScenarios":"`parse_code_from_redirect(\"/auth/callback?code=abc\", Some(\"xyz\"))` — a `code` param present but no `state` — reaching the `else if is_callback_payload` branch. Via `receive_loopback_code` when the redirect from auth.openai.com arrives without state because the authorize request never included one, or a callback-shaped request hits the listener from another source.","commonSituations":"Authorize URL hand-built instead of using `build_authorize_url` (which always embeds state); test fixtures or custom integrations constructing the callback path by hand; a proxy or manual edit stripping query parameters from the redirect.","solutions":["Build the authorize URL with `build_authorize_url(&pkce)` — it always sets `state` (and PKCE fields)","Restart the flow and let the browser complete the full redirect; never hand-type the callback URL","Keep the redirect's query string intact — no rewriting middleware that drops parameters","If parsing redirects yourself, pass the state value unchanged from the authorize request"],"exampleFix":"// before: hand-built authorize URL without state\nlet url = format!(\"{}?response_type=code&client_id={}\", OPENAI_OAUTH_AUTHORIZE_URL, OPENAI_OAUTH_CLIENT_ID);\n// callback arrives with no state -> \"Missing OAuth state in callback\"\n\n// after: helper embeds state and PKCE correctly\nlet pkce = generate_pkce_state();\nlet url = build_authorize_url(&pkce);","handlingStrategy":"validation","validationCode":"fn callback_has_state(input: &str) -> bool {\n    input.split_once('?').map_or(false, |(_, q)| {\n        q.split('&').any(|pair| pair.starts_with(\"state=\"))\n    })\n}\n\nif callback_has_state(path) {\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(\"Missing OAuth state\") => restart_login_flow().await,\n    Err(e) => return Err(e),\n}","preventionTips":["Always build the authorize URL with build_authorize_url — it embeds state","Never strip query parameters from the redirect URI","Keep the loopback listener on the exact registered redirect URI so the browser round-trips state"],"tags":["oauth","openai","state","callback","rust"],"backgroundTag":"oauth-state-missing","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}