siyuan-note/siyuan · error
OAuth issuer mismatch
Error message
OAuth issuer mismatch
What it means
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.
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.
Example fix
// before: trailing-slash discrepancy issuer configured: "https://auth.example.com/" IdP advertises: "https://auth.example.com" // after: make them identical issuer configured: "https://auth.example.com"
Defensive patterns
Strategy: validation
Validate before calling
asm, _ := auth.GetAuthServerMetadata(ctx, authServerURL, client)
if issuerConfigured != asm.Issuer {
// fix the configured issuer to exactly match the IdP's published issuer before starting the flow
} Try / catch
if err := CompleteMCPOAuth(flowID, code, state, callbackErr, issuer); err != nil {
if strings.Contains(err.Error(), "issuer mismatch") {
refreshServerMetadataAndRestart(server)
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- configuration is not initialized
- discover OAuth authorization server
- exchange OAuth authorization code
- mcp oauth authorization required
- OAuth authorization failed
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/6e5f7a1cde2efd95.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/mcp/client/oauth.go:600
}
oauthFlows.Unlock()
}
func CompleteMCPOAuth(flowID, code, state, callbackError, issuer string) error {
oauthFlows.Lock()
flow := oauthFlows.items[flowID]
if flow == nil || time.Now().After(flow.Expires) {
delete(oauthFlows.items, flowID)
oauthFlows.Unlock()
return fmt.Errorf("OAuth flow is missing or expired")
}
if state != flow.State {
oauthFlows.Unlock()
return fmt.Errorf("OAuth state mismatch")
}
if issuer != "" && issuer != flow.Issuer {
oauthFlows.Unlock()
return fmt.Errorf("OAuth issuer mismatch")
}
delete(oauthFlows.items, flowID)
oauthFlows.Unlock()
select {
case flow.Result <- oauthCallbackResult{Code: code, State: state, Error: callbackError}:
return nil
default:
return fmt.Errorf("OAuth callback was already handled")
}
}
func IsLoopbackCallback(remoteAddr string) bool {
host, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
return false
}
ip := net.ParseIP(host)
return ip != nil && ip.IsLoopback()View on GitHub (pinned to 9f775e8a12)