{"record":{"id":"f04223727232a595","repo":"zeroclaw-labs/zeroclaw","slug":"missing-oauth-code-in-callback","errorCode":null,"errorMessage":"Missing OAuth code in callback","messagePattern":"Missing OAuth code in callback","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-providers/src/auth/openai_oauth.rs","lineNumber":337,"sourceCode":"    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)\n        .ok()?;\n    let claims: serde_json::Value = serde_json::from_slice(&decoded).ok()?;\n\n    // Prefer the flat chatgpt_account_id claim when present.\n    if let Some(value) = claims.get(\"chatgpt_account_id\").and_then(|v| v.as_str())\n        && !value.trim().is_empty()\n    {\n        return Some(value.to_string());\n    }\n\n    // Real OpenAI OAuth tokens namespace custom claims under\n    // https://api.openai.com/auth as a JSON object, not a flat dotted key.","sourceCodeStart":319,"sourceCodeEnd":355,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-providers/src/auth/openai_oauth.rs#L319-L355","documentation":"`parse_code_from_redirect` classified the input as a callback payload (contains `?` or has `code`/`state`/`error` params) but found no `code` parameter, so there is nothing to exchange for tokens. Non-callback raw input is returned as-is; only callback-shaped input without a code fails. This prevents an empty or partial redirect from being treated as a successful login.","triggerScenarios":"`parse_code_from_redirect(\"/auth/callback?state=xyz\", Some(\"xyz\"))` — state validates but no `code` key exists in the parsed query. Any query-bearing path that reaches the final bail without a `code` param (the `error` and state checks already passed).","commonSituations":"User manually opens http://localhost:1455/auth/callback before completing consent; the IdP redirects after an aborted consent carrying only state; browser prefetch or a stray request lands on the loopback listener with a query string.","solutions":["Restart the login flow and complete consent in the browser so the redirect carries `code`","Do not navigate to the loopback callback URL manually","Ignore callback-shaped requests that lack `code` (favicon/prefetch noise) instead of failing the flow","If integrating, require the full authorize step to finish before parsing the callback path"],"exampleFix":"// before: first request to the listener aborts the whole login\nlet code = parse_code_from_redirect(path, Some(&state))?; // \"Missing OAuth code in callback\"\n\n// after: keep listening when the hit carries no code\nlet code = match parse_code_from_redirect(path, Some(&state)) {\n    Ok(code) => code,\n    Err(e) if e.to_string().contains(\"Missing OAuth code\") => continue_listening().await,\n    Err(e) => return Err(e),\n};","handlingStrategy":"validation","validationCode":"fn callback_has_code(input: &str) -> bool {\n    input.split_once('?').map_or(false, |(_, q)| {\n        q.split('&').any(|pair| pair.starts_with(\"code=\"))\n    })\n}\n\nif callback_has_code(path) {\n    let code = parse_code_from_redirect(path, Some(&pkce.state))?;\n}","typeGuard":null,"tryCatchPattern":"match parse_code_from_redirect(path, Some(&state)) {\n    Ok(code) => code,\n    Err(e) if e.to_string().contains(\"Missing OAuth code\") => continue_listening().await, // stray hit, keep the listener up\n    Err(e) => return Err(e),\n}","preventionTips":["Treat only requests under /auth/callback carrying code= as login completion","Ignore favicon and prefetch hits on the loopback listener","Require consent to finish in the browser; never hand-navigate to the callback URL"],"tags":["oauth","openai","callback","authorization-code","rust"],"backgroundTag":"oauth-missing-authorization-code","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}