Kuberwastaken/claurst · error
OAuth state mismatch — possible CSRF attack
Error message
OAuth state mismatch — possible CSRF attack
What it means
The OAuth authorization response must echo back the `state` parameter the client generated. If the received state does not match the expected value, the response may be a CSRF forgery, so the library aborts the flow rather than exchanging the code.
Solutions
- Abort the stale browser tab and restart the OAuth login from the CLI
- Verify the provider preserves the state parameter through the redirect
- Avoid running multiple simultaneous MCP auth sessions for the same server
- Clear cached auth state for the server and re-run the auth flow
Defensive patterns
Strategy: try-catch
Try / catch
match run_mcp_auth_session(server).await {
Err(e) if e.to_string().contains("state mismatch") => {
// discard stale tabs, restart the auth flow from scratch
}
other => other?,
} Prevention
- Close stale OAuth tabs from previous login attempts before re-authenticating
- Run only one auth session per server at a time
- Use a provider that preserves the state parameter end-to-end
When it happens
Trigger: wait_for_authorization_code receives a callback whose `state` query parameter is missing or differs from expected_state (passed by run_mcp_auth_session).
Common situations: Stale browser tab from a previous auth attempt completes after a newer session started; the IDP drops or rewrites the state parameter; user manually edited the callback URL; multiple concurrent auth sessions share one callback port.
Related errors
- OAuth state mismatch — possible CSRF attack
- OAuth callback path mismatch: expected
- models endpoint returned
- Invalid JWT: expected at least 2 dot-separated segments
- Invalid : contains unsafe characters
AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10).
Data as JSON: /api/errors/346267df271cd000.
Report an issue: GitHub.
Appendix: source
Thrown at src-rust/crates/mcp/src/oauth.rs:291
.write_all(response.as_bytes())
.await
.map_err(|e| anyhow::anyhow!("Failed to write OAuth callback response: {}", e))?;
if parsed_url.path() != callback_path {
anyhow::bail!(
"OAuth callback path mismatch: expected '{}', got '{}'",
callback_path,
parsed_url.path()
);
}
if let Some(expected_state) = expected_state {
let received_state = parsed_url
.query_pairs()
.find(|(key, _)| key == "state")
.map(|(_, value)| value.to_string());
if received_state.as_deref() != Some(expected_state) {
anyhow::bail!("OAuth state mismatch — possible CSRF attack");
}
}
parsed_url
.query_pairs()
.find(|(key, _)| key == "code")
.map(|(_, value)| value.to_string())
.ok_or_else(|| anyhow::anyhow!("OAuth callback did not contain an authorization code"))
}
pub async fn run_mcp_auth_session(session: McpAuthSession) -> anyhow::Result<McpAuthResult> {
let (listener, host, callback_path) = bind_callback_listener(&session.redirect_uri).await?;
open::that(&session.auth_url)
.map_err(|e| anyhow::anyhow!("Failed to open browser for OAuth: {}", e))?;
let code = wait_for_authorization_code(listener, &host, &callback_path, None).await?;
let mut token = exchange_code(
&session.metadata.token_endpoint,View on GitHub (pinned to b0637c97ec)