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

  1. Restart the OAuth flow so flow.Issuer matches the current IdP issuer.
  2. Compare the configured issuer with the IdP's published issuer exactly (watch trailing slashes and scheme/http vs https).
  3. Update the MCP server's authorization server metadata if the IdP issuer changed.
  4. Ensure the callback route passes the issuer belonging to this flow, not another server's.
  5. 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

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


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)