{"record":{"id":"e70faecb75aa9ad6","repo":"different-ai/openwork","slug":"mcp-oauth-issuer-mismatch","errorCode":"MCP_OAUTH_ISSUER_MISMATCH","errorMessage":"The stored OAuth discovery state no longer matches the issuer bound to this MCP connection.","messagePattern":"The stored OAuth discovery state no longer matches the issuer bound to this MCP connection\\.","errorType":"error_code","errorClass":"EnterpriseMcpOAuthContractError","httpStatus":null,"severity":"error","filePath":"packages/enterprise-mcp-client/src/authorization-response.ts","lineNumber":75,"sourceCode":"\n  const advertisedIssuers = Array.isArray(resourceMetadata?.authorization_servers)\n    ? resourceMetadata.authorization_servers.filter((value): value is string => typeof value === \"string\")\n    : undefined\n  const discoveryIsBound = discoveryIssuer\n    ? isAuthorizationServerDiscoveryBound({\n        authorizationServerUrl: discoveryIssuer,\n        authorizationServerMetadata: metadataIssuer ? { issuer: metadataIssuer } : undefined,\n        resourceMetadata: resourceMetadata\n          ? {\n              resource: optionalString(resourceMetadata.resource),\n              authorization_servers: advertisedIssuers,\n            }\n          : undefined,\n      }, expectedIssuer)\n    : (metadataIssuer === undefined || metadataIssuer === expectedIssuer)\n      && (advertisedIssuers === undefined || advertisedIssuers.includes(expectedIssuer))\n  if (!discoveryIsBound) {\n    throw new EnterpriseMcpOAuthContractError(\n      \"MCP_OAUTH_ISSUER_MISMATCH\",\n      \"The stored OAuth discovery state no longer matches the issuer bound to this MCP connection.\",\n    )\n  }\n\n  if (input.responseIssuer !== undefined) {\n    if (input.responseIssuer !== expectedIssuer) {\n      if (\n        input.mixUpDefense === \"distinct-redirect-uri\"\n        && authorizationServerMetadata?.authorization_response_iss_parameter_supported !== true\n      ) {\n        return {\n          defense: \"distinct-redirect-uri\",\n          ignoredResponseIssuer: input.responseIssuer,\n        }\n      }\n      throw new EnterpriseMcpOAuthContractError(\n        \"MCP_OAUTH_ISSUER_MISMATCH\",","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/packages/enterprise-mcp-client/src/authorization-response.ts#L57-L93","documentation":"The stored OAuth discovery state for an MCP connection must agree with the expected issuer bound to the connection: metadata issuer (when present) must equal the expected issuer, and any authorization_servers advertised in resource metadata must include it. When discoveryIsBound is false, MCP_OAUTH_ISSUER_MISMATCH is thrown because the cached/recorded discovery no longer matches the connection's issuer.","triggerScenarios":"Validating an authorization response where authorizationServerMetadata.issuer differs from expectedIssuer, or the resource metadata's authorization_servers array omits the expected issuer — typically after the AS rotated/renamed its issuer URL or stale discovery state is reused.","commonSituations":"Authorization server changed its issuer URL (rename, migration to new domain) while the client kept cached metadata; resource metadata lists a different set of authorization servers than the one used for this connection; mixing metadata from two ASes (e.g. staging vs production) in one connection; stale cache after an AS upgrade.","solutions":["Re-run authorization-server discovery to refresh stored metadata so it matches the current issuer","Verify expectedIssuer matches the AS's current metadata issuer (check for trailing-slash or URL differences)","Clear stale discovery/cache records for the MCP connection and re-bind it","Ensure resource metadata's authorization_servers includes the issuer used for this connection"],"exampleFix":"// before\nconst metadata = cachedMetadata // issuer: https://old-as.example.com\nvalidateMcpAuthorizationResponseIssuer({ response, expectedIssuer: 'https://new-as.example.com', authorizationServerMetadata: metadata })\n// after\nconst metadata = await discoverAuthorizationServerMetadata('https://new-as.example.com') // fresh, matching issuer\nvalidateMcpAuthorizationResponseIssuer({ response, expectedIssuer: 'https://new-as.example.com', authorizationServerMetadata: metadata })","handlingStrategy":"validation","validationCode":"function assertDiscoveryBound(metadataIssuer, discoveryUrl, expectedIssuer) {\n  const metadataOk = metadataIssuer === undefined || metadataIssuer === expectedIssuer;\n  const urlOk = discoveryUrl === undefined || normalizeIssuer(discoveryUrl) === normalizeIssuer(expectedIssuer);\n  if (!metadataOk || !urlOk) throw new Error(`Stored discovery (${discoveryUrl}) no longer matches expected issuer ${expectedIssuer} — re-run discovery`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  validateMcpAuthorizationResponseIssuer(input)\n} catch (e) {\n  if (e instanceof EnterpriseMcpOAuthContractError && e.code === 'MCP_OAUTH_ISSUER_MISMATCH') {\n    // refresh discovery metadata and re-bind the connection\n  } else throw e\n}","preventionTips":["Refresh discovery metadata on each OAuth flow instead of trusting long-lived caches","Normalize issuer URLs (trailing slash, case) when comparing","Re-bind connections after an AS issuer URL migration","Ensure resource metadata's authorization_servers includes the bound issuer"],"tags":["oauth","mcp","issuer-mismatch","cache-staleness"],"backgroundTag":"oauth-issuer-mismatch","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}