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

  1. Abort the stale browser tab and restart the OAuth login from the CLI
  2. Verify the provider preserves the state parameter through the redirect
  3. Avoid running multiple simultaneous MCP auth sessions for the same server
  4. 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

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


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)