siyuan-note/siyuan · error
OAuth issuer mismatch
Error message
OAuth issuer mismatch
What it means
An optional `issuer` argument supplied to `CompleteMCPOAuth` does not equal the `Issuer` bound to the stored flow. The check only fires when a non-empty `issuer` is passed; its purpose is to bind a callback to a specific authorization server so that a token issued by a different (e.g. spoofed or multi-tenant) issuer is rejected.
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 251596fc0d)
Solutions
- Re-run OAuth metadata discovery for the provider and pass the freshly discovered `issuer` (or omit the `issuer` argument to skip the check).
- Verify the `issuer` passed to the callback matches exactly (scheme, host, port, trailing slash) the issuer stored when the flow was created.
- If the provider legitimately rotates issuers, clear stored credentials and start a new flow so the bound issuer is refreshed.
Defensive patterns
Strategy: validation
Validate before calling
// Re-fetch the issuer from metadata discovery and pass it, or omit it to skip the check.
discovered, err := discoverOAuthMetadata(ctx, serverID)
if err != nil {
return err
}
// Pass discovered.Issuer (or "") to CompleteMCPOAuth.
return CompleteMCPOAuth(flowID, code, state, callbackError, discovered.Issuer) Prevention
- Always source the issuer from the provider's metadata `issuer` field, not the token endpoint URL.
- Omit the issuer argument when issuer binding is not required.
- Restart the flow if the provider legitimately rotates its issuer.
When it happens
Trigger: The callback handler is invoked with an `issuer` string that disagrees with the issuer recorded at flow-creation time. This happens when metadata discovery resolved to a different issuer URL, or when the caller hard-codes an issuer that no longer matches the provider's advertised `issuer` claim.
Common situations: Provider changed its issuer URL after a regional failover or tenant migration. The caller passes the token endpoint URL as the issuer instead of the metadata `issuer` field. A multi-tenant IdP returns tenant-specific issuer values that vary per authorization.
Related errors
- OAuth state mismatch
- OAuth token endpoint returned %s
- OAuth token endpoint returned no access token
- OAuth revocation endpoint must use HTTPS or loopback HTTP
- mcp oauth authorization required
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/6e5f7a1cde2efd95.
Report an issue: GitHub.