{"record":{"id":"6e5f7a1cde2efd95","repo":"siyuan-note/siyuan","slug":"oauth-issuer-mismatch","errorCode":null,"errorMessage":"OAuth issuer mismatch","messagePattern":"OAuth issuer mismatch","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/mcp/client/oauth.go","lineNumber":600,"sourceCode":"\t}\n\toauthFlows.Unlock()\n}\n\nfunc CompleteMCPOAuth(flowID, code, state, callbackError, issuer string) error {\n\toauthFlows.Lock()\n\tflow := oauthFlows.items[flowID]\n\tif flow == nil || time.Now().After(flow.Expires) {\n\t\tdelete(oauthFlows.items, flowID)\n\t\toauthFlows.Unlock()\n\t\treturn fmt.Errorf(\"OAuth flow is missing or expired\")\n\t}\n\tif state != flow.State {\n\t\toauthFlows.Unlock()\n\t\treturn fmt.Errorf(\"OAuth state mismatch\")\n\t}\n\tif issuer != \"\" && issuer != flow.Issuer {\n\t\toauthFlows.Unlock()\n\t\treturn fmt.Errorf(\"OAuth issuer mismatch\")\n\t}\n\tdelete(oauthFlows.items, flowID)\n\toauthFlows.Unlock()\n\tselect {\n\tcase flow.Result <- oauthCallbackResult{Code: code, State: state, Error: callbackError}:\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"OAuth callback was already handled\")\n\t}\n}\n\nfunc IsLoopbackCallback(remoteAddr string) bool {\n\thost, _, err := net.SplitHostPort(remoteAddr)\n\tif err != nil {\n\t\treturn false\n\t}\n\tip := net.ParseIP(host)\n\treturn ip != nil && ip.IsLoopback()","sourceCodeStart":582,"sourceCodeEnd":618,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/mcp/client/oauth.go#L582-L618","documentation":"When the callback includes a non-empty issuer, CompleteMCPOAuth verifies it equals the issuer recorded when the flow started. A mismatch means the authorization server that answered the browser is not the one this flow was created for — a defense against issuer-confusion/mix-up attacks. The flow is not completed.","triggerScenarios":"CompleteMCPOAuth called with issuer != \"\" and issuer != flow.Issuer — e.g. the IdP redirected through a different issuer URL than discovered metadata advertised, or the callback handler passes an issuer from a different server configuration.","commonSituations":"IdP was migrated/renamed mid-flow so its redirect uses a new issuer; server metadata and actual token endpoint disagree on issuer (trailing slash differences); multiple MCP servers with different IdPs and a mixed-up callback routing.","solutions":["Restart the OAuth flow so flow.Issuer matches the current IdP issuer.","Compare the configured issuer with the IdP's published issuer exactly (watch trailing slashes and scheme/http vs https).","Update the MCP server's authorization server metadata if the IdP issuer changed.","Ensure the callback route passes the issuer belonging to this flow, not another server's.","Check IdP multi-tenancy: the tenant answering the redirect must be the one the flow started with."],"exampleFix":"// before: trailing-slash discrepancy\nissuer configured: \"https://auth.example.com/\"\nIdP advertises:    \"https://auth.example.com\"\n// after: make them identical\nissuer configured: \"https://auth.example.com\"","handlingStrategy":"validation","validationCode":"asm, _ := auth.GetAuthServerMetadata(ctx, authServerURL, client)\nif issuerConfigured != asm.Issuer {\n    // fix the configured issuer to exactly match the IdP's published issuer before starting the flow\n}","typeGuard":null,"tryCatchPattern":"if err := CompleteMCPOAuth(flowID, code, state, callbackErr, issuer); err != nil {\n    if strings.Contains(err.Error(), \"issuer mismatch\") {\n        refreshServerMetadataAndRestart(server)\n    }\n}","preventionTips":["Match the configured issuer byte-for-byte with the IdP's advertised issuer (watch trailing slashes)","Refresh server metadata after any IdP migration before starting flows","Route each MCP server's callback to its own flow; do not cross wire issuers","Verify tenant selection at the IdP matches the configured issuer"],"tags":["oauth","mcp","issuer-validation","security"],"backgroundTag":"oauth-issuer-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"}