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

  1. Re-run OAuth metadata discovery for the provider and pass the freshly discovered `issuer` (or omit the `issuer` argument to skip the check).
  2. Verify the `issuer` passed to the callback matches exactly (scheme, host, port, trailing slash) the issuer stored when the flow was created.
  3. 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

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


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/6e5f7a1cde2efd95. Report an issue: GitHub.