{"record":{"id":"ea976f10ea8181fe","repo":"vercel/ai","slug":"oauth-authorization-response-issuer-callbackissu","errorCode":null,"errorMessage":"OAuth authorization response issuer ${callbackIssuer} does not match expected issuer ${expectedIssuer}","messagePattern":"OAuth authorization response issuer (.+?) does not match expected issuer (.+?)","errorType":"exception","errorClass":"MCPClientOAuthError","httpStatus":null,"severity":"error","filePath":"packages/mcp/src/tool/oauth.ts","lineNumber":177,"sourceCode":"  try {\n    validateDownloadUrl(endpointUrl.href);\n  } catch (error) {\n    throw new MCPClientOAuthError({\n      message: `OAuth endpoint URL is not allowed: ${endpointUrl.href}`,\n      cause: error,\n    });\n  }\n}\n\nfunction validateAuthorizationResponseIssuer({\n  callbackIssuer,\n  expectedIssuer,\n}: {\n  callbackIssuer: string | undefined;\n  expectedIssuer: string;\n}): void {\n  if (callbackIssuer != null && callbackIssuer !== expectedIssuer) {\n    throw new MCPClientOAuthError({\n      message: `OAuth authorization response issuer ${callbackIssuer} does not match expected issuer ${expectedIssuer}`,\n    });\n  }\n}\n\nfunction createAuthorizationServerInformation(\n  authorizationServerUrl: string | URL,\n  metadata?: AuthorizationServerMetadata,\n): OAuthAuthorizationServerInformation {\n  return {\n    issuer: metadata?.issuer ?? String(authorizationServerUrl),\n    authorizationServerUrl: normalizeUrl(authorizationServerUrl),\n    tokenEndpoint: normalizeUrl(\n      metadata?.token_endpoint\n        ? new URL(metadata.token_endpoint)\n        : new URL('/token', authorizationServerUrl),\n    ),\n  };","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/mcp/src/tool/oauth.ts#L159-L195","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: expected issuer derived from a different host than the callback iss\nconst expectedIssuer = 'https://auth.example.com';\n// callback: https://login.example.com/oauth/callback?iss=https://login.example.com\n// after: use the issuer exactly as the IdP advertises it\nconst { issuer } = await discoveryMetadata; // 'https://login.example.com/'\nstartAuthFlow({ expectedIssuer: issuer });","handlingStrategy":"try-catch","validationCode":"export function issuerMatchesCallback(expected: string, callbackIssuer?: string): boolean {\n  return callbackIssuer == null || callbackIssuer === expected;\n}\n// before starting auth: normalize and store expectedIssuer from the IdP's discovery metadata","typeGuard":null,"tryCatchPattern":"import { MCPClientOAuthError } from './oauth';\ntry {\n  await client.auth();\n} catch (error) {\n  if (MCPClientOAuthError.isInstance(error) && error.message.includes('does not match expected issuer')) {\n    // read both issuers from the message, fix config/proxy so they match, restart the flow\n  } else {\n    throw error;\n  }\n}","preventionTips":["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."],"tags":["oauth","security","issuer-mismatch","csrf"],"backgroundTag":"oauth-issuer-mismatch","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}