vercel/ai · error · MCPClientOAuthError
OAuth authorization response issuer ${callbackIssuer} does n
Error message
OAuth authorization response issuer ${callbackIssuer} does not match expected issuer ${expectedIssuer} What it means
validateAuthorizationResponseIssuer implements the OAuth 2.0 mixed-up client / issuer-mixup protection: when the authorization response callback carries an `iss` parameter, it must equal the issuer the client started the flow with. If the callback issuer differs from the expected issuer, MCPClientOAuthError is thrown to prevent token injection or cross-IdP attacks. It runs in authInternal after the redirect callback returns.
Source
Thrown at packages/mcp/src/tool/oauth.ts:177
try {
validateDownloadUrl(endpointUrl.href);
} catch (error) {
throw new MCPClientOAuthError({
message: `OAuth endpoint URL is not allowed: ${endpointUrl.href}`,
cause: error,
});
}
}
function validateAuthorizationResponseIssuer({
callbackIssuer,
expectedIssuer,
}: {
callbackIssuer: string | undefined;
expectedIssuer: string;
}): void {
if (callbackIssuer != null && callbackIssuer !== expectedIssuer) {
throw new MCPClientOAuthError({
message: `OAuth authorization response issuer ${callbackIssuer} does not match expected issuer ${expectedIssuer}`,
});
}
}
function createAuthorizationServerInformation(
authorizationServerUrl: string | URL,
metadata?: AuthorizationServerMetadata,
): OAuthAuthorizationServerInformation {
return {
issuer: metadata?.issuer ?? String(authorizationServerUrl),
authorizationServerUrl: normalizeUrl(authorizationServerUrl),
tokenEndpoint: normalizeUrl(
metadata?.token_endpoint
? new URL(metadata.token_endpoint)
: new URL('/token', authorizationServerUrl),
),
};View on GitHub (pinned to 69428b1f8b)
Solutions
- Compare the `iss` value in the error message with the issuer in the authorization server metadata and make your configured/discovered issuer exactly match it (including scheme, host, port, and trailing slash).
- Ensure the same IdP/issuer handles both the start of the flow and the callback; don't switch domains or environments mid-flow.
- If behind a proxy that rewrites hosts, fix proxy headers (X-Forwarded-*) or the IdP's advertised issuer so the returned iss matches.
- For Auth0/multi-tenant providers, use the issuer value from the well-known metadata document, not a manually typed domain.
- Clear stale flow state (stored expected issuer from a previous session) if you recently migrated IdPs and retry the full auth flow.
Example fix
// before: expected issuer derived from a different host than the callback iss
const expectedIssuer = 'https://auth.example.com';
// callback: https://login.example.com/oauth/callback?iss=https://login.example.com
// after: use the issuer exactly as the IdP advertises it
const { issuer } = await discoveryMetadata; // 'https://login.example.com/'
startAuthFlow({ expectedIssuer: issuer }); Defensive patterns
Strategy: try-catch
Validate before calling
export function issuerMatchesCallback(expected: string, callbackIssuer?: string): boolean {
return callbackIssuer == null || callbackIssuer === expected;
}
// before starting auth: normalize and store expectedIssuer from the IdP's discovery metadata Try / catch
import { MCPClientOAuthError } from './oauth';
try {
await client.auth();
} catch (error) {
if (MCPClientOAuthError.isInstance(error) && error.message.includes('does not match expected issuer')) {
// read both issuers from the message, fix config/proxy so they match, restart the flow
} else {
throw error;
}
} Prevention
- Always take the expected issuer from the IdP's well-known metadata, never hand-typed.
- Keep trailing slashes, scheme, host, and port byte-identical between config and IdP metadata.
- Don't switch IdP domains/environments between flow start and callback.
- Fix reverse proxies so hosts/iss aren't rewritten mid-flow.
When it happens
Trigger: The OAuth provider redirects back with an `iss` query parameter that doesn't match the issuer URL configured/discharged from server metadata (e.g. trailing-slash differences, http vs https, different host); routing the callback through a proxy that rewrites the URL; switching IdPs between flow start and callback; a phishing/lookalike authorization server responding with its own issuer.
Common situations: Issuer metadata says `https://auth.example.com` but callback sends `https://auth.example.com/` or `https://login.example.com`; dev/prod issuer mismatch where the local flow was started against staging; multi-tenant IdPs (Auth0 custom domains) where the token endpoint issuer differs from the authorize host; OAuth proxies or gateways rewriting hosts.
Related errors
- OAuth state parameter mismatch - possible CSRF attack
- OAuth endpoint URL is not allowed: ${endpointUrl.href}
- OAuth protected resource metadata URL ${resourceMetadataUrl.
- Protected resource ${resourceMetadata.resource} does not mat
- Tool approval signature verification failed for approval "${
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/ea976f10ea8181fe.
Report an issue: GitHub.