zeroclaw-labs/zeroclaw · error · anyhow::Error
OAuth state mismatch
Error message
OAuth state mismatch
What it means
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.
Source
Thrown at crates/zeroclaw-providers/src/auth/openai_oauth.rs:322
let params = parse_query_params(query);
let is_callback_payload = trimmed.contains('?')
|| params.contains_key("code")
|| params.contains_key("state")
|| params.contains_key("error");
if let Some(err) = params.get("error") {
let desc = params
.get("error_description")
.cloned()
.unwrap_or_else(|| "OAuth authorization failed".to_string());
anyhow::bail!("OpenAI OAuth error: {err} ({desc})");
}
if let Some(expected_state) = expected_state {
if let Some(got) = params.get("state") {
if got != expected_state {
anyhow::bail!("OAuth state mismatch");
}
} else if is_callback_payload {
anyhow::bail!("Missing OAuth state in callback");
}
}
if let Some(code) = params.get("code").cloned() {
return Ok(code);
}
if !is_callback_payload {
return Ok(trimmed.to_string());
}
anyhow::bail!("Missing OAuth code in callback")
}
pub fn extract_account_id_from_jwt(token: &str) -> Option<String> {View on GitHub (pinned to 88bb9c8533)
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
Example fix
// before: URL built with an old PkceState, callback checked against a new one let url = build_authorize_url(&old_pkce); // later, in the callback handler: let code = parse_code_from_redirect(path, Some(&new_pkce.state))?; // "OAuth state mismatch" // after: one PkceState drives the whole flow let pkce = generate_pkce_state(); let url = build_authorize_url(&pkce); // later: let code = parse_code_from_redirect(path, Some(&pkce.state))?;
Defensive patterns
Strategy: validation
Validate before calling
fn state_matches(input: &str, expected: &str) -> bool {
input.split_once('?').map_or(false, |(_, q)| {
q.split('&').any(|pair| pair == format!("state={}", expected))
})
}
// only hand the path to the parser when the state round-trips
if state_matches(path, &pkce.state) {
let code = parse_code_from_redirect(path, Some(&pkce.state))?;
} Try / catch
match parse_code_from_redirect(path, Some(&pkce.state)) {
Ok(code) => exchange(code),
Err(e) if e.to_string().contains("state mismatch") => restart_login_flow().await, // stale attempt: discard
Err(e) => return Err(e),
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Missing OAuth state in callback
- xAI OAuth state mismatch
- OAuth state mismatch
- OAuth state mismatch: expected {expected}, got {actual}
- Missing OAuth code in callback
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/9101f9f762263509.
Report an issue: GitHub.