zed-industries/zed · error
OAuth state mismatch
Error message
OAuth state mismatch
What it means
Raised in do_oauth_flow when the `state` query parameter of the received OAuth callback does not equal the random state value generated for the PKCE authorize request. OAuth state is a CSRF protection binding the browser redirect to this specific flow; a mismatch means the redirect either belongs to a different/concurrent flow, was replayed, or was forged. The library refuses to exchange the authorization code in that case.
Solutions
- Restart sign_in cleanly, ensuring only one OAuth flow runs at a time (cancel any in-flight one first).
- Close stale browser tabs from previous sign-in attempts before retrying.
- Do not disable the check — it protects against CSRF; investigate how a foreign callback reached the local server instead.
- Verify no other application or flow is bound to the same callback port (CALLBACK_PORT) that could deliver mismatched state.
Example fix
// before
let creds = sign_in(&http_client, cx).await?;
// after
// ensure only one flow at a time
if let Some(inflight) = pending_sign_in.take() { inflight.abort(); }
let creds = sign_in(&http_client, cx).await
.inspect_err(|e| if e.to_string().contains("state mismatch") {
log::warn!("Stale or forged OAuth callback rejected; retry sign-in");
})?; Defensive patterns
Strategy: validation
Validate before calling
// before exchanging the code, confirm single-flow ownership of the callback port
let server_bound = std::net::TcpListener::bind((CALLBACK_HOST, CALLBACK_PORT)).is_err();
if !server_bound {
return Err(anyhow!("another OAuth flow may be listening; stale callbacks possible"));
} Type guard
fn state_matches(callback_state: &str, pkce_state: &str) -> bool {
// constant-time comparison to avoid timing side channels
callback_state.len() == pkce_state.len()
&& callback_state.bytes().zip(pkce_state.bytes()).fold(0u8, |acc, (a, b)| acc | (a ^ b)) == 0
} Try / catch
match sign_in(&http_client, cx).await {
Err(e) if e.to_string().contains("OAuth state mismatch") => {
log::warn!("callback state mismatch (CSRF or stale tab); restarting flow");
sign_in(&http_client, cx).await
}
other => other,
} Prevention
- Never compare OAuth state with a plain == if exposing a helper; use constant-time comparison
- Abort previous sign-in flows before starting a new one
- Instruct users to close old sign-in tabs after failed attempts
- Keep the random state per-flow and never persist it across retries
When it happens
Trigger: The callback URL delivered to the local callback server carries a state parameter different from pkce.state — e.g. two sign-in flows ran concurrently and the browser hit the stale one, the user reloaded an old redirect URL, or a malicious page redirected to a forged callback.
Common situations: User opens the authorize URL twice (two tabs/windows) and completes the older flow; leftover browser tab from a previous session redirects to the port now owned by a new server instance; cross-site request forgery attempt against the localhost callback port.
Related errors
- OAuth endpoint must not point to IPv6 unique-local address
- OAuth endpoint must not point to private/reserved IP
- OAuth endpoint must not point to private/reserved IP: ::ffff
- OAuth endpoint must not point to reserved IPv6 address
- Auth server metadata issuer mismatch: expected
AI-assisted analysis of zed-industries/zed@916fc2b8cb (2026-09-19).
Data as JSON: /api/errors/de3d3b4dc21d4142.
Report an issue: GitHub.
Appendix: source
Thrown at crates/x_ai_subscribed/src/x_ai_subscribed.rs:820
oauth_callback_server::OAuthCallbackServerConfig {
host: CALLBACK_HOST,
preferred_port: CALLBACK_PORT,
fallback_port: None,
path: CALLBACK_PATH,
},
)
.context("Failed to start OAuth callback server")?;
let pkce = new_pkce_authorize_request(redirect_uri)?;
cx.update(|cx| cx.open_url(&pkce.authorize_url));
let callback = callback_rx
.await
.map_err(|_| anyhow!("OAuth callback was cancelled"))?
.context("OAuth callback failed")?;
if callback.state != pkce.state {
return Err(anyhow!("OAuth state mismatch"));
}
let tokens = exchange_code(
&http_client,
&callback.code,
&pkce.verifier,
&pkce.redirect_uri,
)
.await
.context("Token exchange failed")?;
let refresh_token = tokens
.refresh_token
.filter(|token| !token.is_empty())
.context("Token response did not include a refresh_token")?;
let email = tokens
.id_token
.as_deref()View on GitHub (pinned to 916fc2b8cb)