auth.js 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943
  1. "use strict";
  2. var __importDefault = (this && this.__importDefault) || function (mod) {
  3. return (mod && mod.__esModule) ? mod : { "default": mod };
  4. };
  5. Object.defineProperty(exports, "__esModule", { value: true });
  6. exports.UnauthorizedError = void 0;
  7. exports.selectClientAuthMethod = selectClientAuthMethod;
  8. exports.parseErrorResponse = parseErrorResponse;
  9. exports.auth = auth;
  10. exports.isHttpsUrl = isHttpsUrl;
  11. exports.selectResourceURL = selectResourceURL;
  12. exports.extractWWWAuthenticateParams = extractWWWAuthenticateParams;
  13. exports.extractResourceMetadataUrl = extractResourceMetadataUrl;
  14. exports.discoverOAuthProtectedResourceMetadata = discoverOAuthProtectedResourceMetadata;
  15. exports.discoverOAuthMetadata = discoverOAuthMetadata;
  16. exports.buildDiscoveryUrls = buildDiscoveryUrls;
  17. exports.discoverAuthorizationServerMetadata = discoverAuthorizationServerMetadata;
  18. exports.discoverOAuthServerInfo = discoverOAuthServerInfo;
  19. exports.startAuthorization = startAuthorization;
  20. exports.prepareAuthorizationCodeRequest = prepareAuthorizationCodeRequest;
  21. exports.exchangeAuthorization = exchangeAuthorization;
  22. exports.refreshAuthorization = refreshAuthorization;
  23. exports.fetchToken = fetchToken;
  24. exports.registerClient = registerClient;
  25. const pkce_challenge_1 = __importDefault(require("pkce-challenge"));
  26. const types_js_1 = require("../types.js");
  27. const auth_js_1 = require("../shared/auth.js");
  28. const auth_js_2 = require("../shared/auth.js");
  29. const auth_utils_js_1 = require("../shared/auth-utils.js");
  30. const errors_js_1 = require("../server/auth/errors.js");
  31. class UnauthorizedError extends Error {
  32. constructor(message) {
  33. super(message ?? 'Unauthorized');
  34. }
  35. }
  36. exports.UnauthorizedError = UnauthorizedError;
  37. function isClientAuthMethod(method) {
  38. return ['client_secret_basic', 'client_secret_post', 'none'].includes(method);
  39. }
  40. const AUTHORIZATION_CODE_RESPONSE_TYPE = 'code';
  41. const AUTHORIZATION_CODE_CHALLENGE_METHOD = 'S256';
  42. /**
  43. * Determines the best client authentication method to use based on server support and client configuration.
  44. *
  45. * Priority order (highest to lowest):
  46. * 1. client_secret_basic (if client secret is available)
  47. * 2. client_secret_post (if client secret is available)
  48. * 3. none (for public clients)
  49. *
  50. * @param clientInformation - OAuth client information containing credentials
  51. * @param supportedMethods - Authentication methods supported by the authorization server
  52. * @returns The selected authentication method
  53. */
  54. function selectClientAuthMethod(clientInformation, supportedMethods) {
  55. const hasClientSecret = clientInformation.client_secret !== undefined;
  56. // Prefer the method returned by the server during client registration, if valid.
  57. // When server metadata is present we also require the method to be listed as supported;
  58. // when supportedMethods is empty (metadata omitted the field) the DCR hint stands alone.
  59. if ('token_endpoint_auth_method' in clientInformation &&
  60. clientInformation.token_endpoint_auth_method &&
  61. isClientAuthMethod(clientInformation.token_endpoint_auth_method) &&
  62. (supportedMethods.length === 0 || supportedMethods.includes(clientInformation.token_endpoint_auth_method))) {
  63. return clientInformation.token_endpoint_auth_method;
  64. }
  65. // If server metadata omits token_endpoint_auth_methods_supported, RFC 8414 §2 says the
  66. // default is client_secret_basic. RFC 6749 §2.3.1 also requires servers to support HTTP
  67. // Basic authentication for clients with a secret, making it the safest default.
  68. if (supportedMethods.length === 0) {
  69. return hasClientSecret ? 'client_secret_basic' : 'none';
  70. }
  71. // Try methods in priority order (most secure first)
  72. if (hasClientSecret && supportedMethods.includes('client_secret_basic')) {
  73. return 'client_secret_basic';
  74. }
  75. if (hasClientSecret && supportedMethods.includes('client_secret_post')) {
  76. return 'client_secret_post';
  77. }
  78. if (supportedMethods.includes('none')) {
  79. return 'none';
  80. }
  81. // Fallback: use what we have
  82. return hasClientSecret ? 'client_secret_post' : 'none';
  83. }
  84. /**
  85. * Applies client authentication to the request based on the specified method.
  86. *
  87. * Implements OAuth 2.1 client authentication methods:
  88. * - client_secret_basic: HTTP Basic authentication (RFC 6749 Section 2.3.1)
  89. * - client_secret_post: Credentials in request body (RFC 6749 Section 2.3.1)
  90. * - none: Public client authentication (RFC 6749 Section 2.1)
  91. *
  92. * @param method - The authentication method to use
  93. * @param clientInformation - OAuth client information containing credentials
  94. * @param headers - HTTP headers object to modify
  95. * @param params - URL search parameters to modify
  96. * @throws {Error} When required credentials are missing
  97. */
  98. function applyClientAuthentication(method, clientInformation, headers, params) {
  99. const { client_id, client_secret } = clientInformation;
  100. switch (method) {
  101. case 'client_secret_basic':
  102. applyBasicAuth(client_id, client_secret, headers);
  103. return;
  104. case 'client_secret_post':
  105. applyPostAuth(client_id, client_secret, params);
  106. return;
  107. case 'none':
  108. applyPublicAuth(client_id, params);
  109. return;
  110. default:
  111. throw new Error(`Unsupported client authentication method: ${method}`);
  112. }
  113. }
  114. /**
  115. * Applies HTTP Basic authentication (RFC 6749 Section 2.3.1)
  116. */
  117. function applyBasicAuth(clientId, clientSecret, headers) {
  118. if (!clientSecret) {
  119. throw new Error('client_secret_basic authentication requires a client_secret');
  120. }
  121. const credentials = btoa(`${clientId}:${clientSecret}`);
  122. headers.set('Authorization', `Basic ${credentials}`);
  123. }
  124. /**
  125. * Applies POST body authentication (RFC 6749 Section 2.3.1)
  126. */
  127. function applyPostAuth(clientId, clientSecret, params) {
  128. params.set('client_id', clientId);
  129. if (clientSecret) {
  130. params.set('client_secret', clientSecret);
  131. }
  132. }
  133. /**
  134. * Applies public client authentication (RFC 6749 Section 2.1)
  135. */
  136. function applyPublicAuth(clientId, params) {
  137. params.set('client_id', clientId);
  138. }
  139. /**
  140. * Parses an OAuth error response from a string or Response object.
  141. *
  142. * If the input is a standard OAuth2.0 error response, it will be parsed according to the spec
  143. * and an instance of the appropriate OAuthError subclass will be returned.
  144. * If parsing fails, it falls back to a generic ServerError that includes
  145. * the response status (if available) and original content.
  146. *
  147. * @param input - A Response object or string containing the error response
  148. * @returns A Promise that resolves to an OAuthError instance
  149. */
  150. async function parseErrorResponse(input) {
  151. const statusCode = input instanceof Response ? input.status : undefined;
  152. const body = input instanceof Response ? await input.text() : input;
  153. try {
  154. const result = auth_js_1.OAuthErrorResponseSchema.parse(JSON.parse(body));
  155. const { error, error_description, error_uri } = result;
  156. const errorClass = errors_js_1.OAUTH_ERRORS[error] || errors_js_1.ServerError;
  157. return new errorClass(error_description || '', error_uri);
  158. }
  159. catch (error) {
  160. // Not a valid OAuth error response, but try to inform the user of the raw data anyway
  161. const errorMessage = `${statusCode ? `HTTP ${statusCode}: ` : ''}Invalid OAuth error response: ${error}. Raw body: ${body}`;
  162. return new errors_js_1.ServerError(errorMessage);
  163. }
  164. }
  165. /**
  166. * Orchestrates the full auth flow with a server.
  167. *
  168. * This can be used as a single entry point for all authorization functionality,
  169. * instead of linking together the other lower-level functions in this module.
  170. */
  171. async function auth(provider, options) {
  172. try {
  173. return await authInternal(provider, options);
  174. }
  175. catch (error) {
  176. // Handle recoverable error types by invalidating credentials and retrying
  177. if (error instanceof errors_js_1.InvalidClientError || error instanceof errors_js_1.UnauthorizedClientError) {
  178. await provider.invalidateCredentials?.('all');
  179. return await authInternal(provider, options);
  180. }
  181. else if (error instanceof errors_js_1.InvalidGrantError) {
  182. await provider.invalidateCredentials?.('tokens');
  183. return await authInternal(provider, options);
  184. }
  185. // Throw otherwise
  186. throw error;
  187. }
  188. }
  189. async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
  190. // Check if the provider has cached discovery state to skip discovery
  191. const cachedState = await provider.discoveryState?.();
  192. let resourceMetadata;
  193. let authorizationServerUrl;
  194. let metadata;
  195. // If resourceMetadataUrl is not provided, try to load it from cached state
  196. // This handles browser redirects where the URL was saved before navigation
  197. let effectiveResourceMetadataUrl = resourceMetadataUrl;
  198. if (!effectiveResourceMetadataUrl && cachedState?.resourceMetadataUrl) {
  199. effectiveResourceMetadataUrl = new URL(cachedState.resourceMetadataUrl);
  200. }
  201. if (cachedState?.authorizationServerUrl) {
  202. // Restore discovery state from cache
  203. authorizationServerUrl = cachedState.authorizationServerUrl;
  204. resourceMetadata = cachedState.resourceMetadata;
  205. metadata =
  206. cachedState.authorizationServerMetadata ?? (await discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn }));
  207. // If resource metadata wasn't cached, try to fetch it for selectResourceURL
  208. if (!resourceMetadata) {
  209. try {
  210. resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl }, fetchFn);
  211. }
  212. catch {
  213. // RFC 9728 not available — selectResourceURL will handle undefined
  214. }
  215. }
  216. // Re-save if we enriched the cached state with missing metadata
  217. if (metadata !== cachedState.authorizationServerMetadata || resourceMetadata !== cachedState.resourceMetadata) {
  218. await provider.saveDiscoveryState?.({
  219. authorizationServerUrl: String(authorizationServerUrl),
  220. resourceMetadataUrl: effectiveResourceMetadataUrl?.toString(),
  221. resourceMetadata,
  222. authorizationServerMetadata: metadata
  223. });
  224. }
  225. }
  226. else {
  227. // Full discovery via RFC 9728
  228. const serverInfo = await discoverOAuthServerInfo(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl, fetchFn });
  229. authorizationServerUrl = serverInfo.authorizationServerUrl;
  230. metadata = serverInfo.authorizationServerMetadata;
  231. resourceMetadata = serverInfo.resourceMetadata;
  232. // Persist discovery state for future use
  233. // TODO: resourceMetadataUrl is only populated when explicitly provided via options
  234. // or loaded from cached state. The URL derived internally by
  235. // discoverOAuthProtectedResourceMetadata() is not captured back here.
  236. await provider.saveDiscoveryState?.({
  237. authorizationServerUrl: String(authorizationServerUrl),
  238. resourceMetadataUrl: effectiveResourceMetadataUrl?.toString(),
  239. resourceMetadata,
  240. authorizationServerMetadata: metadata
  241. });
  242. }
  243. const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
  244. // Apply scope selection strategy (SEP-835):
  245. // 1. WWW-Authenticate scope (passed via `scope` param)
  246. // 2. PRM scopes_supported
  247. // 3. Client metadata scope (user-configured fallback)
  248. // The resolved scope is used consistently for both DCR and the authorization request.
  249. const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope;
  250. // Handle client registration if needed
  251. let clientInformation = await Promise.resolve(provider.clientInformation());
  252. if (!clientInformation) {
  253. if (authorizationCode !== undefined) {
  254. throw new Error('Existing OAuth client information is required when exchanging an authorization code');
  255. }
  256. const supportsUrlBasedClientId = metadata?.client_id_metadata_document_supported === true;
  257. const clientMetadataUrl = provider.clientMetadataUrl;
  258. if (clientMetadataUrl && !isHttpsUrl(clientMetadataUrl)) {
  259. throw new errors_js_1.InvalidClientMetadataError(`clientMetadataUrl must be a valid HTTPS URL with a non-root pathname, got: ${clientMetadataUrl}`);
  260. }
  261. const shouldUseUrlBasedClientId = supportsUrlBasedClientId && clientMetadataUrl;
  262. if (shouldUseUrlBasedClientId) {
  263. // SEP-991: URL-based Client IDs
  264. clientInformation = {
  265. client_id: clientMetadataUrl
  266. };
  267. await provider.saveClientInformation?.(clientInformation);
  268. }
  269. else {
  270. // Fallback to dynamic registration
  271. if (!provider.saveClientInformation) {
  272. throw new Error('OAuth client information must be saveable for dynamic registration');
  273. }
  274. const fullInformation = await registerClient(authorizationServerUrl, {
  275. metadata,
  276. clientMetadata: provider.clientMetadata,
  277. scope: resolvedScope,
  278. fetchFn
  279. });
  280. await provider.saveClientInformation(fullInformation);
  281. clientInformation = fullInformation;
  282. }
  283. }
  284. // Non-interactive flows (e.g., client_credentials, jwt-bearer) don't need a redirect URL
  285. const nonInteractiveFlow = !provider.redirectUrl;
  286. // Exchange authorization code for tokens, or fetch tokens directly for non-interactive flows
  287. if (authorizationCode !== undefined || nonInteractiveFlow) {
  288. const tokens = await fetchToken(provider, authorizationServerUrl, {
  289. metadata,
  290. resource,
  291. authorizationCode,
  292. fetchFn
  293. });
  294. await provider.saveTokens(tokens);
  295. return 'AUTHORIZED';
  296. }
  297. const tokens = await provider.tokens();
  298. // Handle token refresh or new authorization
  299. if (tokens?.refresh_token) {
  300. try {
  301. // Attempt to refresh the token
  302. const newTokens = await refreshAuthorization(authorizationServerUrl, {
  303. metadata,
  304. clientInformation,
  305. refreshToken: tokens.refresh_token,
  306. resource,
  307. addClientAuthentication: provider.addClientAuthentication,
  308. fetchFn
  309. });
  310. await provider.saveTokens(newTokens);
  311. return 'AUTHORIZED';
  312. }
  313. catch (error) {
  314. // If this is a ServerError, or an unknown type, log it out and try to continue. Otherwise, escalate so we can fix things and retry.
  315. if (!(error instanceof errors_js_1.OAuthError) || error instanceof errors_js_1.ServerError) {
  316. // Could not refresh OAuth tokens
  317. }
  318. else {
  319. // Refresh failed for another reason, re-throw
  320. throw error;
  321. }
  322. }
  323. }
  324. const state = provider.state ? await provider.state() : undefined;
  325. // Start new authorization flow
  326. const { authorizationUrl, codeVerifier } = await startAuthorization(authorizationServerUrl, {
  327. metadata,
  328. clientInformation,
  329. state,
  330. redirectUrl: provider.redirectUrl,
  331. scope: resolvedScope,
  332. resource
  333. });
  334. await provider.saveCodeVerifier(codeVerifier);
  335. await provider.redirectToAuthorization(authorizationUrl);
  336. return 'REDIRECT';
  337. }
  338. /**
  339. * SEP-991: URL-based Client IDs
  340. * Validate that the client_id is a valid URL with https scheme
  341. */
  342. function isHttpsUrl(value) {
  343. if (!value)
  344. return false;
  345. try {
  346. const url = new URL(value);
  347. return url.protocol === 'https:' && url.pathname !== '/';
  348. }
  349. catch {
  350. return false;
  351. }
  352. }
  353. async function selectResourceURL(serverUrl, provider, resourceMetadata) {
  354. const defaultResource = (0, auth_utils_js_1.resourceUrlFromServerUrl)(serverUrl);
  355. // If provider has custom validation, delegate to it
  356. if (provider.validateResourceURL) {
  357. return await provider.validateResourceURL(defaultResource, resourceMetadata?.resource);
  358. }
  359. // Only include resource parameter when Protected Resource Metadata is present
  360. if (!resourceMetadata) {
  361. return undefined;
  362. }
  363. // Validate that the metadata's resource is compatible with our request
  364. if (!(0, auth_utils_js_1.checkResourceAllowed)({ requestedResource: defaultResource, configuredResource: resourceMetadata.resource })) {
  365. throw new Error(`Protected resource ${resourceMetadata.resource} does not match expected ${defaultResource} (or origin)`);
  366. }
  367. // Prefer the resource from metadata since it's what the server is telling us to request
  368. return new URL(resourceMetadata.resource);
  369. }
  370. /**
  371. * Extract resource_metadata, scope, and error from WWW-Authenticate header.
  372. */
  373. function extractWWWAuthenticateParams(res) {
  374. const authenticateHeader = res.headers.get('WWW-Authenticate');
  375. if (!authenticateHeader) {
  376. return {};
  377. }
  378. const [type, scheme] = authenticateHeader.split(' ');
  379. if (type.toLowerCase() !== 'bearer' || !scheme) {
  380. return {};
  381. }
  382. const resourceMetadataMatch = extractFieldFromWwwAuth(res, 'resource_metadata') || undefined;
  383. let resourceMetadataUrl;
  384. if (resourceMetadataMatch) {
  385. try {
  386. resourceMetadataUrl = new URL(resourceMetadataMatch);
  387. }
  388. catch {
  389. // Ignore invalid URL
  390. }
  391. }
  392. const scope = extractFieldFromWwwAuth(res, 'scope') || undefined;
  393. const error = extractFieldFromWwwAuth(res, 'error') || undefined;
  394. return {
  395. resourceMetadataUrl,
  396. scope,
  397. error
  398. };
  399. }
  400. /**
  401. * Extracts a specific field's value from the WWW-Authenticate header string.
  402. *
  403. * @param response The HTTP response object containing the headers.
  404. * @param fieldName The name of the field to extract (e.g., "realm", "nonce").
  405. * @returns The field value
  406. */
  407. function extractFieldFromWwwAuth(response, fieldName) {
  408. const wwwAuthHeader = response.headers.get('WWW-Authenticate');
  409. if (!wwwAuthHeader) {
  410. return null;
  411. }
  412. const pattern = new RegExp(`${fieldName}=(?:"([^"]+)"|([^\\s,]+))`);
  413. const match = wwwAuthHeader.match(pattern);
  414. if (match) {
  415. // Pattern matches: field_name="value" or field_name=value (unquoted)
  416. return match[1] || match[2];
  417. }
  418. return null;
  419. }
  420. /**
  421. * Extract resource_metadata from response header.
  422. * @deprecated Use `extractWWWAuthenticateParams` instead.
  423. */
  424. function extractResourceMetadataUrl(res) {
  425. const authenticateHeader = res.headers.get('WWW-Authenticate');
  426. if (!authenticateHeader) {
  427. return undefined;
  428. }
  429. const [type, scheme] = authenticateHeader.split(' ');
  430. if (type.toLowerCase() !== 'bearer' || !scheme) {
  431. return undefined;
  432. }
  433. const regex = /resource_metadata="([^"]*)"/;
  434. const match = regex.exec(authenticateHeader);
  435. if (!match) {
  436. return undefined;
  437. }
  438. try {
  439. return new URL(match[1]);
  440. }
  441. catch {
  442. return undefined;
  443. }
  444. }
  445. /**
  446. * Looks up RFC 9728 OAuth 2.0 Protected Resource Metadata.
  447. *
  448. * If the server returns a 404 for the well-known endpoint, this function will
  449. * return `undefined`. Any other errors will be thrown as exceptions.
  450. */
  451. async function discoverOAuthProtectedResourceMetadata(serverUrl, opts, fetchFn = fetch) {
  452. const response = await discoverMetadataWithFallback(serverUrl, 'oauth-protected-resource', fetchFn, {
  453. protocolVersion: opts?.protocolVersion,
  454. metadataUrl: opts?.resourceMetadataUrl
  455. });
  456. if (!response || response.status === 404) {
  457. await response?.body?.cancel();
  458. throw new Error(`Resource server does not implement OAuth 2.0 Protected Resource Metadata.`);
  459. }
  460. if (!response.ok) {
  461. await response.body?.cancel();
  462. throw new Error(`HTTP ${response.status} trying to load well-known OAuth protected resource metadata.`);
  463. }
  464. return auth_js_2.OAuthProtectedResourceMetadataSchema.parse(await response.json());
  465. }
  466. /**
  467. * Helper function to handle fetch with CORS retry logic
  468. */
  469. async function fetchWithCorsRetry(url, headers, fetchFn = fetch) {
  470. try {
  471. return await fetchFn(url, { headers });
  472. }
  473. catch (error) {
  474. if (error instanceof TypeError) {
  475. if (headers) {
  476. // CORS errors come back as TypeError, retry without headers
  477. return fetchWithCorsRetry(url, undefined, fetchFn);
  478. }
  479. else {
  480. // We're getting CORS errors on retry too, return undefined
  481. return undefined;
  482. }
  483. }
  484. throw error;
  485. }
  486. }
  487. /**
  488. * Constructs the well-known path for auth-related metadata discovery
  489. */
  490. function buildWellKnownPath(wellKnownPrefix, pathname = '', options = {}) {
  491. // Strip trailing slash from pathname to avoid double slashes
  492. if (pathname.endsWith('/')) {
  493. pathname = pathname.slice(0, -1);
  494. }
  495. return options.prependPathname ? `${pathname}/.well-known/${wellKnownPrefix}` : `/.well-known/${wellKnownPrefix}${pathname}`;
  496. }
  497. /**
  498. * Tries to discover OAuth metadata at a specific URL
  499. */
  500. async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch) {
  501. const headers = {
  502. 'MCP-Protocol-Version': protocolVersion
  503. };
  504. return await fetchWithCorsRetry(url, headers, fetchFn);
  505. }
  506. /**
  507. * Determines if fallback to root discovery should be attempted
  508. */
  509. function shouldAttemptFallback(response, pathname) {
  510. return !response || (response.status >= 400 && response.status < 500 && pathname !== '/');
  511. }
  512. /**
  513. * Generic function for discovering OAuth metadata with fallback support
  514. */
  515. async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, opts) {
  516. const issuer = new URL(serverUrl);
  517. const protocolVersion = opts?.protocolVersion ?? types_js_1.LATEST_PROTOCOL_VERSION;
  518. let url;
  519. if (opts?.metadataUrl) {
  520. url = new URL(opts.metadataUrl);
  521. }
  522. else {
  523. // Try path-aware discovery first
  524. const wellKnownPath = buildWellKnownPath(wellKnownType, issuer.pathname);
  525. url = new URL(wellKnownPath, opts?.metadataServerUrl ?? issuer);
  526. url.search = issuer.search;
  527. }
  528. let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn);
  529. // If path-aware discovery fails with 404 and we're not already at root, try fallback to root discovery
  530. if (!opts?.metadataUrl && shouldAttemptFallback(response, issuer.pathname)) {
  531. const rootUrl = new URL(`/.well-known/${wellKnownType}`, issuer);
  532. response = await tryMetadataDiscovery(rootUrl, protocolVersion, fetchFn);
  533. }
  534. return response;
  535. }
  536. /**
  537. * Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.
  538. *
  539. * If the server returns a 404 for the well-known endpoint, this function will
  540. * return `undefined`. Any other errors will be thrown as exceptions.
  541. *
  542. * @deprecated This function is deprecated in favor of `discoverAuthorizationServerMetadata`.
  543. */
  544. async function discoverOAuthMetadata(issuer, { authorizationServerUrl, protocolVersion } = {}, fetchFn = fetch) {
  545. if (typeof issuer === 'string') {
  546. issuer = new URL(issuer);
  547. }
  548. if (!authorizationServerUrl) {
  549. authorizationServerUrl = issuer;
  550. }
  551. if (typeof authorizationServerUrl === 'string') {
  552. authorizationServerUrl = new URL(authorizationServerUrl);
  553. }
  554. protocolVersion ?? (protocolVersion = types_js_1.LATEST_PROTOCOL_VERSION);
  555. const response = await discoverMetadataWithFallback(authorizationServerUrl, 'oauth-authorization-server', fetchFn, {
  556. protocolVersion,
  557. metadataServerUrl: authorizationServerUrl
  558. });
  559. if (!response || response.status === 404) {
  560. await response?.body?.cancel();
  561. return undefined;
  562. }
  563. if (!response.ok) {
  564. await response.body?.cancel();
  565. throw new Error(`HTTP ${response.status} trying to load well-known OAuth metadata`);
  566. }
  567. return auth_js_2.OAuthMetadataSchema.parse(await response.json());
  568. }
  569. /**
  570. * Builds a list of discovery URLs to try for authorization server metadata.
  571. * URLs are returned in priority order:
  572. * 1. OAuth metadata at the given URL
  573. * 2. OIDC metadata endpoints at the given URL
  574. */
  575. function buildDiscoveryUrls(authorizationServerUrl) {
  576. const url = typeof authorizationServerUrl === 'string' ? new URL(authorizationServerUrl) : authorizationServerUrl;
  577. const hasPath = url.pathname !== '/';
  578. const urlsToTry = [];
  579. if (!hasPath) {
  580. // Root path: https://example.com/.well-known/oauth-authorization-server
  581. urlsToTry.push({
  582. url: new URL('/.well-known/oauth-authorization-server', url.origin),
  583. type: 'oauth'
  584. });
  585. // OIDC: https://example.com/.well-known/openid-configuration
  586. urlsToTry.push({
  587. url: new URL(`/.well-known/openid-configuration`, url.origin),
  588. type: 'oidc'
  589. });
  590. return urlsToTry;
  591. }
  592. // Strip trailing slash from pathname to avoid double slashes
  593. let pathname = url.pathname;
  594. if (pathname.endsWith('/')) {
  595. pathname = pathname.slice(0, -1);
  596. }
  597. // 1. OAuth metadata at the given URL
  598. // Insert well-known before the path: https://example.com/.well-known/oauth-authorization-server/tenant1
  599. urlsToTry.push({
  600. url: new URL(`/.well-known/oauth-authorization-server${pathname}`, url.origin),
  601. type: 'oauth'
  602. });
  603. // 2. OIDC metadata endpoints
  604. // RFC 8414 style: Insert /.well-known/openid-configuration before the path
  605. urlsToTry.push({
  606. url: new URL(`/.well-known/openid-configuration${pathname}`, url.origin),
  607. type: 'oidc'
  608. });
  609. // OIDC Discovery 1.0 style: Append /.well-known/openid-configuration after the path
  610. urlsToTry.push({
  611. url: new URL(`${pathname}/.well-known/openid-configuration`, url.origin),
  612. type: 'oidc'
  613. });
  614. return urlsToTry;
  615. }
  616. /**
  617. * Discovers authorization server metadata with support for RFC 8414 OAuth 2.0 Authorization Server Metadata
  618. * and OpenID Connect Discovery 1.0 specifications.
  619. *
  620. * This function implements a fallback strategy for authorization server discovery:
  621. * 1. Attempts RFC 8414 OAuth metadata discovery first
  622. * 2. If OAuth discovery fails, falls back to OpenID Connect Discovery
  623. *
  624. * @param authorizationServerUrl - The authorization server URL obtained from the MCP Server's
  625. * protected resource metadata, or the MCP server's URL if the
  626. * metadata was not found.
  627. * @param options - Configuration options
  628. * @param options.fetchFn - Optional fetch function for making HTTP requests, defaults to global fetch
  629. * @param options.protocolVersion - MCP protocol version to use, defaults to LATEST_PROTOCOL_VERSION
  630. * @returns Promise resolving to authorization server metadata, or undefined if discovery fails
  631. */
  632. async function discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn = fetch, protocolVersion = types_js_1.LATEST_PROTOCOL_VERSION } = {}) {
  633. const headers = {
  634. 'MCP-Protocol-Version': protocolVersion,
  635. Accept: 'application/json'
  636. };
  637. // Get the list of URLs to try
  638. const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
  639. // Try each URL in order
  640. for (const { url: endpointUrl, type } of urlsToTry) {
  641. const response = await fetchWithCorsRetry(endpointUrl, headers, fetchFn);
  642. if (!response) {
  643. /**
  644. * CORS error occurred - don't throw as the endpoint may not allow CORS,
  645. * continue trying other possible endpoints
  646. */
  647. continue;
  648. }
  649. if (!response.ok) {
  650. await response.body?.cancel();
  651. // Continue looking for any 4xx response code.
  652. if (response.status >= 400 && response.status < 500) {
  653. continue; // Try next URL
  654. }
  655. throw new Error(`HTTP ${response.status} trying to load ${type === 'oauth' ? 'OAuth' : 'OpenID provider'} metadata from ${endpointUrl}`);
  656. }
  657. // Parse and validate based on type
  658. if (type === 'oauth') {
  659. return auth_js_2.OAuthMetadataSchema.parse(await response.json());
  660. }
  661. else {
  662. return auth_js_1.OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
  663. }
  664. }
  665. return undefined;
  666. }
  667. /**
  668. * Discovers the authorization server for an MCP server following
  669. * {@link https://datatracker.ietf.org/doc/html/rfc9728 | RFC 9728} (OAuth 2.0 Protected
  670. * Resource Metadata), with fallback to treating the server URL as the
  671. * authorization server.
  672. *
  673. * This function combines two discovery steps into one call:
  674. * 1. Probes `/.well-known/oauth-protected-resource` on the MCP server to find the
  675. * authorization server URL (RFC 9728).
  676. * 2. Fetches authorization server metadata from that URL (RFC 8414 / OpenID Connect Discovery).
  677. *
  678. * Use this when you need the authorization server metadata for operations outside the
  679. * {@linkcode auth} orchestrator, such as token refresh or token revocation.
  680. *
  681. * @param serverUrl - The MCP resource server URL
  682. * @param opts - Optional configuration
  683. * @param opts.resourceMetadataUrl - Override URL for the protected resource metadata endpoint
  684. * @param opts.fetchFn - Custom fetch function for HTTP requests
  685. * @returns Authorization server URL, metadata, and resource metadata (if available)
  686. */
  687. async function discoverOAuthServerInfo(serverUrl, opts) {
  688. let resourceMetadata;
  689. let authorizationServerUrl;
  690. try {
  691. resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: opts?.resourceMetadataUrl }, opts?.fetchFn);
  692. if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) {
  693. authorizationServerUrl = resourceMetadata.authorization_servers[0];
  694. }
  695. }
  696. catch {
  697. // RFC 9728 not supported -- fall back to treating the server URL as the authorization server
  698. }
  699. // If we don't get a valid authorization server from protected resource metadata,
  700. // fall back to the legacy MCP spec behavior: MCP server base URL acts as the authorization server
  701. if (!authorizationServerUrl) {
  702. authorizationServerUrl = String(new URL('/', serverUrl));
  703. }
  704. const authorizationServerMetadata = await discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn: opts?.fetchFn });
  705. return {
  706. authorizationServerUrl,
  707. authorizationServerMetadata,
  708. resourceMetadata
  709. };
  710. }
  711. /**
  712. * Begins the authorization flow with the given server, by generating a PKCE challenge and constructing the authorization URL.
  713. */
  714. async function startAuthorization(authorizationServerUrl, { metadata, clientInformation, redirectUrl, scope, state, resource }) {
  715. let authorizationUrl;
  716. if (metadata) {
  717. authorizationUrl = new URL(metadata.authorization_endpoint);
  718. if (!metadata.response_types_supported.includes(AUTHORIZATION_CODE_RESPONSE_TYPE)) {
  719. throw new Error(`Incompatible auth server: does not support response type ${AUTHORIZATION_CODE_RESPONSE_TYPE}`);
  720. }
  721. if (metadata.code_challenge_methods_supported &&
  722. !metadata.code_challenge_methods_supported.includes(AUTHORIZATION_CODE_CHALLENGE_METHOD)) {
  723. throw new Error(`Incompatible auth server: does not support code challenge method ${AUTHORIZATION_CODE_CHALLENGE_METHOD}`);
  724. }
  725. }
  726. else {
  727. authorizationUrl = new URL('/authorize', authorizationServerUrl);
  728. }
  729. // Generate PKCE challenge
  730. const challenge = await (0, pkce_challenge_1.default)();
  731. const codeVerifier = challenge.code_verifier;
  732. const codeChallenge = challenge.code_challenge;
  733. authorizationUrl.searchParams.set('response_type', AUTHORIZATION_CODE_RESPONSE_TYPE);
  734. authorizationUrl.searchParams.set('client_id', clientInformation.client_id);
  735. authorizationUrl.searchParams.set('code_challenge', codeChallenge);
  736. authorizationUrl.searchParams.set('code_challenge_method', AUTHORIZATION_CODE_CHALLENGE_METHOD);
  737. authorizationUrl.searchParams.set('redirect_uri', String(redirectUrl));
  738. if (state) {
  739. authorizationUrl.searchParams.set('state', state);
  740. }
  741. if (scope) {
  742. authorizationUrl.searchParams.set('scope', scope);
  743. }
  744. if (scope?.includes('offline_access')) {
  745. // if the request includes the OIDC-only "offline_access" scope,
  746. // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
  747. // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
  748. authorizationUrl.searchParams.append('prompt', 'consent');
  749. }
  750. if (resource) {
  751. authorizationUrl.searchParams.set('resource', resource.href);
  752. }
  753. return { authorizationUrl, codeVerifier };
  754. }
  755. /**
  756. * Prepares token request parameters for an authorization code exchange.
  757. *
  758. * This is the default implementation used by fetchToken when the provider
  759. * doesn't implement prepareTokenRequest.
  760. *
  761. * @param authorizationCode - The authorization code received from the authorization endpoint
  762. * @param codeVerifier - The PKCE code verifier
  763. * @param redirectUri - The redirect URI used in the authorization request
  764. * @returns URLSearchParams for the authorization_code grant
  765. */
  766. function prepareAuthorizationCodeRequest(authorizationCode, codeVerifier, redirectUri) {
  767. return new URLSearchParams({
  768. grant_type: 'authorization_code',
  769. code: authorizationCode,
  770. code_verifier: codeVerifier,
  771. redirect_uri: String(redirectUri)
  772. });
  773. }
  774. /**
  775. * Internal helper to execute a token request with the given parameters.
  776. * Used by exchangeAuthorization, refreshAuthorization, and fetchToken.
  777. */
  778. async function executeTokenRequest(authorizationServerUrl, { metadata, tokenRequestParams, clientInformation, addClientAuthentication, resource, fetchFn }) {
  779. const tokenUrl = metadata?.token_endpoint ? new URL(metadata.token_endpoint) : new URL('/token', authorizationServerUrl);
  780. const headers = new Headers({
  781. 'Content-Type': 'application/x-www-form-urlencoded',
  782. Accept: 'application/json'
  783. });
  784. if (resource) {
  785. tokenRequestParams.set('resource', resource.href);
  786. }
  787. if (addClientAuthentication) {
  788. await addClientAuthentication(headers, tokenRequestParams, tokenUrl, metadata);
  789. }
  790. else if (clientInformation) {
  791. const supportedMethods = metadata?.token_endpoint_auth_methods_supported ?? [];
  792. const authMethod = selectClientAuthMethod(clientInformation, supportedMethods);
  793. applyClientAuthentication(authMethod, clientInformation, headers, tokenRequestParams);
  794. }
  795. const response = await (fetchFn ?? fetch)(tokenUrl, {
  796. method: 'POST',
  797. headers,
  798. body: tokenRequestParams
  799. });
  800. if (!response.ok) {
  801. throw await parseErrorResponse(response);
  802. }
  803. return auth_js_2.OAuthTokensSchema.parse(await response.json());
  804. }
  805. /**
  806. * Exchanges an authorization code for an access token with the given server.
  807. *
  808. * Supports multiple client authentication methods as specified in OAuth 2.1:
  809. * - Automatically selects the best authentication method based on server support
  810. * - Falls back to appropriate defaults when server metadata is unavailable
  811. *
  812. * @param authorizationServerUrl - The authorization server's base URL
  813. * @param options - Configuration object containing client info, auth code, etc.
  814. * @returns Promise resolving to OAuth tokens
  815. * @throws {Error} When token exchange fails or authentication is invalid
  816. */
  817. async function exchangeAuthorization(authorizationServerUrl, { metadata, clientInformation, authorizationCode, codeVerifier, redirectUri, resource, addClientAuthentication, fetchFn }) {
  818. const tokenRequestParams = prepareAuthorizationCodeRequest(authorizationCode, codeVerifier, redirectUri);
  819. return executeTokenRequest(authorizationServerUrl, {
  820. metadata,
  821. tokenRequestParams,
  822. clientInformation,
  823. addClientAuthentication,
  824. resource,
  825. fetchFn
  826. });
  827. }
  828. /**
  829. * Exchange a refresh token for an updated access token.
  830. *
  831. * Supports multiple client authentication methods as specified in OAuth 2.1:
  832. * - Automatically selects the best authentication method based on server support
  833. * - Preserves the original refresh token if a new one is not returned
  834. *
  835. * @param authorizationServerUrl - The authorization server's base URL
  836. * @param options - Configuration object containing client info, refresh token, etc.
  837. * @returns Promise resolving to OAuth tokens (preserves original refresh_token if not replaced)
  838. * @throws {Error} When token refresh fails or authentication is invalid
  839. */
  840. async function refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken, resource, addClientAuthentication, fetchFn }) {
  841. const tokenRequestParams = new URLSearchParams({
  842. grant_type: 'refresh_token',
  843. refresh_token: refreshToken
  844. });
  845. const tokens = await executeTokenRequest(authorizationServerUrl, {
  846. metadata,
  847. tokenRequestParams,
  848. clientInformation,
  849. addClientAuthentication,
  850. resource,
  851. fetchFn
  852. });
  853. // Preserve original refresh token if server didn't return a new one
  854. return { refresh_token: refreshToken, ...tokens };
  855. }
  856. /**
  857. * Unified token fetching that works with any grant type via provider.prepareTokenRequest().
  858. *
  859. * This function provides a single entry point for obtaining tokens regardless of the
  860. * OAuth grant type. The provider's prepareTokenRequest() method determines which grant
  861. * to use and supplies the grant-specific parameters.
  862. *
  863. * @param provider - OAuth client provider that implements prepareTokenRequest()
  864. * @param authorizationServerUrl - The authorization server's base URL
  865. * @param options - Configuration for the token request
  866. * @returns Promise resolving to OAuth tokens
  867. * @throws {Error} When provider doesn't implement prepareTokenRequest or token fetch fails
  868. *
  869. * @example
  870. * // Provider for client_credentials:
  871. * class MyProvider implements OAuthClientProvider {
  872. * prepareTokenRequest(scope) {
  873. * const params = new URLSearchParams({ grant_type: 'client_credentials' });
  874. * if (scope) params.set('scope', scope);
  875. * return params;
  876. * }
  877. * // ... other methods
  878. * }
  879. *
  880. * const tokens = await fetchToken(provider, authServerUrl, { metadata });
  881. */
  882. async function fetchToken(provider, authorizationServerUrl, { metadata, resource, authorizationCode, fetchFn } = {}) {
  883. const scope = provider.clientMetadata.scope;
  884. // Use provider's prepareTokenRequest if available, otherwise fall back to authorization_code
  885. let tokenRequestParams;
  886. if (provider.prepareTokenRequest) {
  887. tokenRequestParams = await provider.prepareTokenRequest(scope);
  888. }
  889. // Default to authorization_code grant if no custom prepareTokenRequest
  890. if (!tokenRequestParams) {
  891. if (!authorizationCode) {
  892. throw new Error('Either provider.prepareTokenRequest() or authorizationCode is required');
  893. }
  894. if (!provider.redirectUrl) {
  895. throw new Error('redirectUrl is required for authorization_code flow');
  896. }
  897. const codeVerifier = await provider.codeVerifier();
  898. tokenRequestParams = prepareAuthorizationCodeRequest(authorizationCode, codeVerifier, provider.redirectUrl);
  899. }
  900. const clientInformation = await provider.clientInformation();
  901. return executeTokenRequest(authorizationServerUrl, {
  902. metadata,
  903. tokenRequestParams,
  904. clientInformation: clientInformation ?? undefined,
  905. addClientAuthentication: provider.addClientAuthentication,
  906. resource,
  907. fetchFn
  908. });
  909. }
  910. /**
  911. * Performs OAuth 2.0 Dynamic Client Registration according to RFC 7591.
  912. *
  913. * If `scope` is provided, it overrides `clientMetadata.scope` in the registration
  914. * request body. This allows callers to apply the Scope Selection Strategy (SEP-835)
  915. * consistently across both DCR and the subsequent authorization request.
  916. */
  917. async function registerClient(authorizationServerUrl, { metadata, clientMetadata, scope, fetchFn }) {
  918. let registrationUrl;
  919. if (metadata) {
  920. if (!metadata.registration_endpoint) {
  921. throw new Error('Incompatible auth server: does not support dynamic client registration');
  922. }
  923. registrationUrl = new URL(metadata.registration_endpoint);
  924. }
  925. else {
  926. registrationUrl = new URL('/register', authorizationServerUrl);
  927. }
  928. const response = await (fetchFn ?? fetch)(registrationUrl, {
  929. method: 'POST',
  930. headers: {
  931. 'Content-Type': 'application/json'
  932. },
  933. body: JSON.stringify({
  934. ...clientMetadata,
  935. ...(scope !== undefined ? { scope } : {})
  936. })
  937. });
  938. if (!response.ok) {
  939. throw await parseErrorResponse(response);
  940. }
  941. return auth_js_2.OAuthClientInformationFullSchema.parse(await response.json());
  942. }
  943. //# sourceMappingURL=auth.js.map