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

  1. 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).
  2. Ensure the same IdP/issuer handles both the start of the flow and the callback; don't switch domains or environments mid-flow.
  3. If behind a proxy that rewrites hosts, fix proxy headers (X-Forwarded-*) or the IdP's advertised issuer so the returned iss matches.
  4. For Auth0/multi-tenant providers, use the issuer value from the well-known metadata document, not a manually typed domain.
  5. 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

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


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/ea976f10ea8181fe. Report an issue: GitHub.