{"record":{"id":"0f6c21ac5dcbb88a","repo":"zeroclaw-labs/zeroclaw","slug":"oauth-state-mismatch-expected-expected-got-ac","errorCode":null,"errorMessage":"OAuth state mismatch: expected {expected}, got {actual}","messagePattern":"OAuth state mismatch: expected (.+?), got (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-providers/src/auth/gemini_oauth.rs","lineNumber":511,"sourceCode":"    }\n\n    // Extract query string\n    let query = if let Some((_, right)) = trimmed.split_once('?') {\n        right\n    } else {\n        trimmed\n    };\n\n    let params = parse_query_params(query);\n\n    // If we have code param, extract it\n    if let Some(code) = params.get(\"code\") {\n        // Validate state if expected\n        if let Some(expected) = expected_state\n            && let Some(actual) = params.get(\"state\")\n            && actual != expected\n        {\n            anyhow::bail!(\"OAuth state mismatch: expected {expected}, got {actual}\");\n        }\n        return Ok(code.clone());\n    }\n\n    // Otherwise, assume it's the raw code (if long enough and no spaces)\n    if trimmed.len() > 10 && !trimmed.contains(' ') && !trimmed.contains('&') {\n        return Ok(trimmed.to_string());\n    }\n\n    anyhow::bail!(\"Could not parse OAuth code from input\")\n}\n\n/// Extract account email from Google ID token.\npub fn extract_account_email_from_id_token(id_token: &str) -> Option<String> {\n    let parts: Vec<&str> = id_token.split('.').collect();\n    if parts.len() != 3 {\n        return None;\n    }","sourceCodeStart":493,"sourceCodeEnd":529,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-providers/src/auth/gemini_oauth.rs#L493-L529","documentation":"parse_code_from_redirect found a code parameter in the pasted input, but the accompanying state parameter does not equal expected_state (the PKCE state of the pending login saved by auth login). The message prints both values. Validation only runs when an expected state was supplied and the input actually carries a state parameter — this is the paste-redirect counterpart of the loopback CSRF check.","triggerScenarios":"Calling parse_code_from_redirect(input, Some(expected)) via receive_code_from_stdin or auth paste-redirect with a URL whose state belongs to a different authorize request: the user copied the callback URL of a previous login attempt, or a new auth login was started (new pending state saved) and the user then pasted the old browser URL.","commonSituations":"Old browser tab finished after a re-run of auth login; two pending logins for different profiles; user pasted a URL truncated or modified so state no longer matches.","solutions":["Re-run auth login for the provider/profile, then copy the callback URL from that same browser session and paste it promptly","Ensure no second login overwrites the pending state file between opening the authorize URL and pasting the redirect","As a fallback, paste only the raw code value (the code=... portion) — the raw-code path skips state validation"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Compare the pasted URL's state against the pending login state before parsing.\nif let Some((_, q)) = pasted_url.split_once('?') {\n    let params = zeroclaw_providers::auth::oauth_common::parse_query_params(q);\n    if let (Some(expected), Some(actual)) = (pending.state.as_str(), params.get(\"state\")) {\n        anyhow::ensure!(actual == expected, \"stale redirect: state differs from pending login\");\n    }\n}\nlet code = parse_code_from_redirect(pasted_url, Some(&pending.state))?;","typeGuard":null,"tryCatchPattern":"match parse_code_from_redirect(input, Some(&pending.state)) {\n    Ok(code) => code,\n    Err(e) if e.to_string().starts_with(\"OAuth state mismatch\") => {\n        eprintln!(\"this URL is from an older login; re-run auth login and paste the fresh URL\");\n        return Err(e);\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Always paste the redirect URL produced by the current login session","Do not start a second login between opening the authorize URL and pasting","Prefer the raw code paste when the full URL keeps mismatching"],"tags":["oauth","csrf","state-mismatch","paste-redirect","gemini","rust"],"backgroundTag":"oauth-state-mismatch","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}