{"record":{"id":"7c7aebe3e7de7b2f","repo":"siyuan-note/siyuan","slug":"oauth-state-mismatch","errorCode":null,"errorMessage":"OAuth state mismatch","messagePattern":"OAuth state mismatch","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/mcp/client/oauth.go","lineNumber":389,"sourceCode":"\toauthFlows.Unlock()\n\tdefer removeOAuthFlow(flowID, flow)\n\tsetMCPRuntimeStateForContext(ctx, h.server.ID, \"authorizing\", 0, \"\", authorizationURL)\n\n\tvar callback oauthCallbackResult\n\ttimer := time.NewTimer(oauthAuthorizationTimeout)\n\tdefer timer.Stop()\n\tselect {\n\tcase callback = <-flow.Result:\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-timer.C:\n\t\treturn fmt.Errorf(\"OAuth authorization timed out\")\n\t}\n\tif callback.Error != \"\" {\n\t\treturn fmt.Errorf(\"OAuth authorization failed: %s\", callback.Error)\n\t}\n\tif callback.State != state {\n\t\treturn fmt.Errorf(\"OAuth state mismatch\")\n\t}\n\tif callback.Code == \"\" {\n\t\treturn fmt.Errorf(\"OAuth callback did not include an authorization code\")\n\t}\n\n\texchangeCtx := context.WithValue(ctx, oauth2.HTTPClient, h.client)\n\ttoken, err := config.Exchange(exchangeCtx, callback.Code,\n\t\toauth2.VerifierOption(verifier),\n\t\toauth2.SetAuthURLParam(\"resource\", prm.Resource))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"exchange OAuth authorization code: %w\", err)\n\t}\n\tif token.TokenType != \"\" && !strings.EqualFold(token.TokenType, \"Bearer\") {\n\t\treturn fmt.Errorf(\"OAuth token endpoint returned unsupported token type %q\", token.TokenType)\n\t}\n\tcredential = registrationCredential\n\tcredential.TokenAuthMethod = authMethod\n\tcredential.AccessToken = token.AccessToken","sourceCodeStart":371,"sourceCodeEnd":407,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/mcp/client/oauth.go#L371-L407","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":"// In CompleteMCPOAuth, mismatch indicates a stale/replayed callback — do NOT retry with same state.\nif state != flow.State {\n    return fmt.Errorf(\"OAuth state mismatch\") // discard; user must start a new flow\n}","preventionTips":["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."],"tags":["oauth","mcp","csrf","state","security","callback"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}