siyuan-note/siyuan · error
OAuth state mismatch
Error message
OAuth state mismatch
What it means
Returned at oauth.go:388-389 when the callback's state parameter does not equal the state originally generated for this flow (oauth.go:268). The state is a 24-byte cryptographically random value bound to the flow; mismatch indicates a stale, replayed, or tampered callback.
Source
Thrown at kernel/mcp/client/oauth.go:389
oauthFlows.Unlock()
defer removeOAuthFlow(flowID, flow)
setMCPRuntimeStateForContext(ctx, h.server.ID, "authorizing", 0, "", authorizationURL)
var callback oauthCallbackResult
timer := time.NewTimer(oauthAuthorizationTimeout)
defer timer.Stop()
select {
case callback = <-flow.Result:
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return fmt.Errorf("OAuth authorization timed out")
}
if callback.Error != "" {
return fmt.Errorf("OAuth authorization failed: %s", callback.Error)
}
if callback.State != state {
return fmt.Errorf("OAuth state mismatch")
}
if callback.Code == "" {
return fmt.Errorf("OAuth callback did not include an authorization code")
}
exchangeCtx := context.WithValue(ctx, oauth2.HTTPClient, h.client)
token, err := config.Exchange(exchangeCtx, callback.Code,
oauth2.VerifierOption(verifier),
oauth2.SetAuthURLParam("resource", prm.Resource))
if err != nil {
return fmt.Errorf("exchange OAuth authorization code: %w", err)
}
if token.TokenType != "" && !strings.EqualFold(token.TokenType, "Bearer") {
return fmt.Errorf("OAuth token endpoint returned unsupported token type %q", token.TokenType)
}
credential = registrationCredential
credential.TokenAuthMethod = authMethod
credential.AccessToken = token.AccessTokenView on GitHub (pinned to 251596fc0d)
Solutions
- Close stale authorization tabs and re-initiate a single fresh flow.
- Ensure only one MCP authorization flow runs at a time per server to avoid flowID/state cross-routing.
- Confirm the AS echoes back the exact state it received in the authorization request (some ASes truncate or strip query params).
Defensive patterns
Strategy: validation
Try / catch
// In CompleteMCPOAuth, mismatch indicates a stale/replayed callback — do NOT retry with same state.
if state != flow.State {
return fmt.Errorf("OAuth state mismatch") // discard; user must start a new flow
} Prevention
- Run only one authorization flow at a time per MCP server to prevent state/flowID cross-routing.
- Close stale authorization browser tabs before starting a new flow.
- Never cache or reuse state/flowID across attempts.
When it happens
Trigger: CompleteMCPOAuth passes a state that differs from flow.State — e.g. an old browser tab from a previous authorization attempt delivers its redirect after a new flow started, or a second concurrent flow's callback is routed to the wrong flowID.
Common situations: User has multiple authorization tabs/windows open; a previous flow's redirect arrives late; callback URL manipulated; flowID reused incorrectly so a different flow's state is expected.
Related errors
- OAuth authorization timed out
- OAuth authorization failed: %s
- OAuth callback did not include an authorization code
- OAuth flow is missing or expired
- OAuth issuer mismatch
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/7c7aebe3e7de7b2f.
Report an issue: GitHub.