zeroclaw-labs/zeroclaw · error
OAuth state mismatch: expected {expected}, got {actual}
Error message
OAuth state mismatch: expected {expected}, got {actual} What it means
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.
Source
Thrown at crates/zeroclaw-providers/src/auth/gemini_oauth.rs:511
}
// Extract query string
let query = if let Some((_, right)) = trimmed.split_once('?') {
right
} else {
trimmed
};
let params = parse_query_params(query);
// If we have code param, extract it
if let Some(code) = params.get("code") {
// Validate state if expected
if let Some(expected) = expected_state
&& let Some(actual) = params.get("state")
&& actual != expected
{
anyhow::bail!("OAuth state mismatch: expected {expected}, got {actual}");
}
return Ok(code.clone());
}
// Otherwise, assume it's the raw code (if long enough and no spaces)
if trimmed.len() > 10 && !trimmed.contains(' ') && !trimmed.contains('&') {
return Ok(trimmed.to_string());
}
anyhow::bail!("Could not parse OAuth code from input")
}
/// Extract account email from Google ID token.
pub fn extract_account_email_from_id_token(id_token: &str) -> Option<String> {
let parts: Vec<&str> = id_token.split('.').collect();
if parts.len() != 3 {
return None;
}View on GitHub (pinned to 88bb9c8533)
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
Defensive patterns
Strategy: validation
Validate before calling
// Compare the pasted URL's state against the pending login state before parsing.
if let Some((_, q)) = pasted_url.split_once('?') {
let params = zeroclaw_providers::auth::oauth_common::parse_query_params(q);
if let (Some(expected), Some(actual)) = (pending.state.as_str(), params.get("state")) {
anyhow::ensure!(actual == expected, "stale redirect: state differs from pending login");
}
}
let code = parse_code_from_redirect(pasted_url, Some(&pending.state))?; Try / catch
match parse_code_from_redirect(input, Some(&pending.state)) {
Ok(code) => code,
Err(e) if e.to_string().starts_with("OAuth state mismatch") => {
eprintln!("this URL is from an older login; re-run auth login and paste the fresh URL");
return Err(e);
}
Err(e) => return Err(e),
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- OAuth state mismatch
- Google device code request failed ({}): {}
- Device code expired before authorization was completed
- User denied authorization
- Device code expired
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/0f6c21ac5dcbb88a.
Report an issue: GitHub.