{"record":{"id":"e3e45a1b7ae8ff84","repo":"vercel/ai","slug":"oauth-endpoint-url-is-not-allowed-endpointurl-h","errorCode":null,"errorMessage":"OAuth endpoint URL is not allowed: ${endpointUrl.href}","messagePattern":"OAuth endpoint URL is not allowed: (.+?)","errorType":"exception","errorClass":"MCPClientOAuthError","httpStatus":null,"severity":"error","filePath":"packages/mcp/src/tool/oauth.ts","lineNumber":162,"sourceCode":" * Loopback is allowed for local OAuth; every other target uses the shared\n * download URL guard (http(s) only, no private/link-local IPs).\n *\n * Credential POSTs use `redirect: 'error'` instead of\n * `fetchWithValidatedRedirects`, which is GET-only and would follow hops with\n * the authorization code, PKCE verifier, and client secret still attached.\n */\nfunction assertSafeOAuthEndpoint(endpointUrl: URL): void {\n  if (\n    (endpointUrl.protocol === 'http:' || endpointUrl.protocol === 'https:') &&\n    isOAuthLoopbackHost(endpointUrl.hostname)\n  ) {\n    return;\n  }\n\n  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  }","sourceCodeStart":144,"sourceCodeEnd":180,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/mcp/src/tool/oauth.ts#L144-L180","documentation":"assertSafeOAuthEndpoint validates that OAuth endpoint URLs (token, refresh, registration endpoints) returned by the authorization server are safe to fetch, using the same validateDownloadUrl checks applied to other downloaded URLs (SSRF protection: scheme/origin restrictions such as disallowing non-http(s) or private addresses). If validation fails, MCPClientOAuthError is thrown with the offending URL in the message. This runs in exchangeAuthorization, refreshAuthorization, and registerClient, so any OAuth flow step touching a disallowed endpoint aborts.","triggerScenarios":"The authorization server's metadata returns a token/registration endpoint URL with a disallowed scheme (e.g. http:// on a non-localhost host) or pointing at a blocked/private IP; a malicious or misconfigured IdP advertises endpoints on intranet addresses; a self-hosted MCP server lists `http://localhost:...` or `http://10.x.x.x` endpoints which the validator rejects; DNS of a host resolves to a private address at validation time.","commonSituations":"Self-hosted OAuth servers behind VPN/private networks; dev environments using http:// endpoints for an internal IdP; corporate IdPs with internal-only metadata URLs; a compromised or buggy IdP metadata document; local testing with non-localhost LAN addresses.","solutions":["Inspect the endpointUrl in the error message and fix the authorization server metadata so it advertises a public https:// URL.","For local development, use http://localhost/... endpoints (typically allowed) instead of LAN IPs like http://192.168.x.x.","Expose the internal OAuth server through a public/reverse-proxy https URL and update the IdP metadata accordingly.","If you control validation policy, check validateDownloadUrl's rules in @ai-sdk/provider-utils and align your endpoints with the allowed scheme/origin rules.","If the URL is actually safe and the rejection is due to DNS resolving to a private address, correct the DNS/proxy setup rather than bypassing validation."],"exampleFix":"// before: IdP metadata advertises an internal endpoint\n{ \"token_endpoint\": \"http://10.0.0.5/oauth/token\" }\n// after: public https endpoint\n{ \"token_endpoint\": \"https://auth.example.com/oauth/token\" }","handlingStrategy":"try-catch","validationCode":"export function isProbablyAllowedEndpoint(url: string): boolean {\n  try {\n    const u = new URL(url);\n    const localhost = u.hostname === 'localhost' || u.hostname === '127.0.0.1';\n    return u.protocol === 'https:' || (u.protocol === 'http:' && localhost);\n  } catch {\n    return false;\n  }\n}\n// pre-check token_endpoint/registration_endpoint from IdP metadata before starting the flow","typeGuard":null,"tryCatchPattern":"import { MCPClientOAuthError } from './oauth';\ntry {\n  await client.auth();\n} catch (error) {\n  if (MCPClientOAuthError.isInstance(error) && error.message.startsWith('OAuth endpoint URL is not allowed')) {\n    // fix IdP metadata to use a public https endpoint, then retry\n  } else {\n    throw error;\n  }\n}","preventionTips":["Host all OAuth endpoints on public https URLs.","For local dev use http://localhost, never LAN/private IPs.","Fetch and review the IdP's well-known metadata for endpoint URLs before wiring up MCP auth.","Avoid self-signed/internal-CA setups that resolve to intranet addresses behind VPN."],"tags":["oauth","security","ssrf","url-validation"],"backgroundTag":"oauth-endpoint-url-not-allowed","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}