{"record":{"id":"7c7aebe3e7de7b2f","repo":"siyuan-note/siyuan","slug":"oauth-state-mismatch","errorCode":null,"errorMessage":"OAuth state mismatch","messagePattern":"OAuth state mismatch","errorType":"exception","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/9f775e8a12daef8255556097396f9b2739078892/kernel/mcp/client/oauth.go#L371-L407","documentation":"The OAuth state parameter is a random per-flow value used to bind the callback to the initiation request and prevent CSRF. When the callback's state does not equal the state generated for the flow, the library rejects the response as a possible CSRF/interception attempt rather than using the code.","triggerScenarios":"A browser request hits the local callback endpoint whose state query parameter differs from the state stored in oauthFlows for that flowID — a stale/duplicate tab, a replayed redirect, or a forged callback.","commonSituations":"User completed the flow twice in two tabs so one redirect hits the wrong/expired flow; a bookmarked or replayed callback URL; an attacker-crafted callback; proxy stripping/altering query parameters.","solutions":["Restart the authorization flow and use only a single browser tab/window for consent","Do not reuse or bookmark old callback URLs; each flow has a unique state","Ensure no proxy or extension rewrites query parameters on the localhost callback","Clear stale tabs of the IdP consent page and retry"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := h.Authorize(ctx, true); err != nil {\n    if strings.Contains(err.Error(), \"state mismatch\") {\n        // discard the stale callback and restart a fresh authorization flow\n    }\n}","preventionTips":["Use a single browser tab per authorization flow and don't reuse old consent tabs","Never bookmark or replay callback URLs — each contains a one-time state","Don't start two Authorize flows concurrently for the same server","Ensure no middleware rewrites query parameters on the localhost callback"],"tags":["oauth","mcp","csrf","state-mismatch"],"backgroundTag":"checksum-mismatch","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}