@@ -7,7 +7,10 @@ import {
77 OAuthMetadata ,
88 OAuthClientInformationFull ,
99 OAuthProtectedResourceMetadata ,
10- OAuthErrorResponseSchema
10+ OAuthErrorResponseSchema ,
11+ OpenIdProviderDiscoveryMetadata ,
12+ AuthorizationServerMetadata ,
13+ OpenIdProviderDiscoveryMetadataSchema
1114} from "../shared/auth.js" ;
1215import { OAuthClientInformationFullSchema , OAuthMetadataSchema , OAuthProtectedResourceMetadataSchema , OAuthTokensSchema } from "../shared/auth.js" ;
1316import { checkResourceAllowed , resourceUrlFromServerUrl } from "../shared/auth-utils.js" ;
@@ -108,7 +111,7 @@ export interface OAuthClientProvider {
108111 * @param url - The token endpoint URL being called
109112 * @param metadata - Optional OAuth metadata for the server, which may include supported authentication methods
110113 */
111- addClientAuthentication ?( headers : Headers , params : URLSearchParams , url : string | URL , metadata ?: OAuthMetadata ) : void | Promise < void > ;
114+ addClientAuthentication ?( headers : Headers , params : URLSearchParams , url : string | URL , metadata ?: AuthorizationServerMetadata ) : void | Promise < void > ;
112115
113116 /**
114117 * If defined, overrides the selection and validation of the
@@ -319,7 +322,7 @@ async function authInternal(
319322) : Promise < AuthResult > {
320323
321324 let resourceMetadata : OAuthProtectedResourceMetadata | undefined ;
322- let authorizationServerUrl = serverUrl ;
325+ let authorizationServerUrl : string | URL | undefined ;
323326 try {
324327 resourceMetadata = await discoverOAuthProtectedResourceMetadata ( serverUrl , { resourceMetadataUrl } , fetchFn ) ;
325328 if ( resourceMetadata . authorization_servers && resourceMetadata . authorization_servers . length > 0 ) {
@@ -331,9 +334,17 @@ async function authInternal(
331334
332335 const resource : URL | undefined = await selectResourceURL ( serverUrl , provider , resourceMetadata ) ;
333336
334- const metadata = await discoverOAuthMetadata ( serverUrl , {
335- authorizationServerUrl
336- } , fetchFn ) ;
337+ const metadata = await discoverAuthorizationServerMetadata ( serverUrl , authorizationServerUrl , {
338+ fetchFn,
339+ } ) ;
340+
341+ /**
342+ * If we don't get a valid authorization server metadata from protected resource metadata,
343+ * fallback to the legacy MCP spec's implementation (version 2025-03-26): MCP server acts as the Authorization server.
344+ */
345+ if ( ! authorizationServerUrl ) {
346+ authorizationServerUrl = serverUrl ;
347+ }
337348
338349 // Handle client registration if needed
339350 let clientInformation = await Promise . resolve ( provider . clientInformation ( ) ) ;
@@ -524,15 +535,21 @@ async function fetchWithCorsRetry(
524535}
525536
526537/**
527- * Constructs the well-known path for OAuth metadata discovery
538+ * Constructs the well-known path for auth-related metadata discovery
528539 */
529- function buildWellKnownPath ( wellKnownPrefix : string , pathname : string ) : string {
530- let wellKnownPath = `/.well-known/${ wellKnownPrefix } ${ pathname } ` ;
540+ function buildWellKnownPath (
541+ wellKnownPrefix : 'oauth-authorization-server' | 'oauth-protected-resource' | 'openid-configuration' ,
542+ pathname : string = '' ,
543+ options : { prependPathname ?: boolean } = { }
544+ ) : string {
545+ // Strip trailing slash from pathname to avoid double slashes
531546 if ( pathname . endsWith ( '/' ) ) {
532- // Strip trailing slash from pathname to avoid double slashes
533- wellKnownPath = wellKnownPath . slice ( 0 , - 1 ) ;
547+ pathname = pathname . slice ( 0 , - 1 ) ;
534548 }
535- return wellKnownPath ;
549+
550+ return options . prependPathname
551+ ? `${ pathname } /.well-known/${ wellKnownPrefix } `
552+ : `/.well-known/${ wellKnownPrefix } ${ pathname } ` ;
536553}
537554
538555/**
@@ -594,6 +611,8 @@ async function discoverMetadataWithFallback(
594611 *
595612 * If the server returns a 404 for the well-known endpoint, this function will
596613 * return `undefined`. Any other errors will be thrown as exceptions.
614+ *
615+ * @deprecated This function is deprecated in favor of `discoverAuthorizationServerMetadata`.
597616 */
598617export async function discoverOAuthMetadata (
599618 issuer : string | URL ,
@@ -640,6 +659,218 @@ export async function discoverOAuthMetadata(
640659 return OAuthMetadataSchema . parse ( await response . json ( ) ) ;
641660}
642661
662+ /**
663+ * Discovers authorization server metadata with support for RFC 8414 OAuth 2.0 Authorization Server Metadata
664+ * and OpenID Connect Discovery 1.0 specifications.
665+ *
666+ * This function implements a fallback strategy for authorization server discovery:
667+ * 1. If `authorizationServerUrl` is provided, attempts RFC 8414 OAuth metadata discovery first
668+ * 2. If OAuth discovery fails, falls back to OpenID Connect Discovery
669+ * 3. If `authorizationServerUrl` is not provided, uses legacy MCP specification behavior
670+ *
671+ * @param serverUrl - The MCP Server URL, used for legacy specification support where the MCP server
672+ * acts as both the resource server and authorization server
673+ * @param authorizationServerUrl - The authorization server URL obtained from the MCP Server's
674+ * protected resource metadata. If this parameter is `undefined`,
675+ * it indicates that protected resource metadata was not successfully
676+ * retrieved, triggering legacy fallback behavior
677+ * @param options - Configuration options
678+ * @param options.fetchFn - Optional fetch function for making HTTP requests, defaults to global fetch
679+ * @param options.protocolVersion - MCP protocol version to use, defaults to LATEST_PROTOCOL_VERSION
680+ * @returns Promise resolving to authorization server metadata, or undefined if discovery fails
681+ */
682+ export async function discoverAuthorizationServerMetadata (
683+ serverUrl : string | URL ,
684+ authorizationServerUrl ?: string | URL ,
685+ {
686+ fetchFn = fetch ,
687+ protocolVersion = LATEST_PROTOCOL_VERSION ,
688+ } : {
689+ fetchFn ?: FetchLike ;
690+ protocolVersion ?: string ;
691+ } = { }
692+ ) : Promise < AuthorizationServerMetadata | undefined > {
693+ if ( ! authorizationServerUrl ) {
694+ // Legacy support: MCP servers act as the Auth server.
695+ return retrieveOAuthMetadataFromMcpServer ( serverUrl , {
696+ fetchFn,
697+ protocolVersion,
698+ } ) ;
699+ }
700+
701+ const oauthMetadata = await retrieveOAuthMetadataFromAuthorizationServer ( authorizationServerUrl , {
702+ fetchFn,
703+ protocolVersion,
704+ } ) ;
705+
706+ if ( oauthMetadata ) {
707+ return oauthMetadata ;
708+ }
709+
710+ return retrieveOpenIdProviderMetadataFromAuthorizationServer ( authorizationServerUrl , {
711+ fetchFn,
712+ protocolVersion,
713+ } ) ;
714+ }
715+
716+ /**
717+ * Legacy implementation where the MCP server acts as the Auth server.
718+ * According to MCP spec version 2025-03-26.
719+ *
720+ * @param serverUrl - The MCP Server URL
721+ * @param options - Configuration options
722+ * @param options.fetchFn - Optional fetch function for making HTTP requests, defaults to global fetch
723+ * @param options.protocolVersion - MCP protocol version to use (required)
724+ * @returns Promise resolving to OAuth metadata
725+ */
726+ async function retrieveOAuthMetadataFromMcpServer (
727+ serverUrl : string | URL ,
728+ {
729+ fetchFn = fetch ,
730+ protocolVersion,
731+ } : {
732+ fetchFn ?: FetchLike ;
733+ protocolVersion : string ;
734+ }
735+ ) : Promise < OAuthMetadata > {
736+ const serverOrigin = typeof serverUrl === 'string' ? new URL ( serverUrl ) . origin : serverUrl . origin ;
737+
738+ const metadataEndpoint = new URL ( buildWellKnownPath ( 'oauth-authorization-server' ) , serverOrigin ) ;
739+
740+ const response = await fetchWithCorsRetry ( metadataEndpoint , getProtocolVersionHeader ( protocolVersion ) , fetchFn ) ;
741+
742+ if ( ! response ) {
743+ throw new Error ( `CORS error trying to load OAuth metadata from ${ metadataEndpoint } ` ) ;
744+ }
745+
746+ if ( ! response . ok ) {
747+ if ( response . status === 404 ) {
748+ /**
749+ * The MCP server does not implement OAuth 2.0 Authorization Server Metadata
750+ *
751+ * Return fallback OAuth 2.0 Authorization Server Metadata
752+ */
753+ return {
754+ issuer : serverOrigin ,
755+ authorization_endpoint : new URL ( '/authorize' , serverOrigin ) . href ,
756+ token_endpoint : new URL ( '/token' , serverOrigin ) . href ,
757+ registration_endpoint : new URL ( '/register' , serverOrigin ) . href ,
758+ response_types_supported : [ 'code' ] ,
759+ } ;
760+ }
761+
762+ throw new Error ( `HTTP ${ response . status } trying to load OAuth metadata from ${ metadataEndpoint } ` ) ;
763+ }
764+
765+ return OAuthMetadataSchema . parse ( await response . json ( ) ) ;
766+ }
767+
768+ /**
769+ * Retrieves RFC 8414 OAuth 2.0 Authorization Server Metadata from the authorization server.
770+ *
771+ * @param authorizationServerUrl - The authorization server URL
772+ * @param options - Configuration options
773+ * @param options.fetchFn - Optional fetch function for making HTTP requests, defaults to global fetch
774+ * @param options.protocolVersion - MCP protocol version to use (required)
775+ * @returns Promise resolving to OAuth metadata, or undefined if discovery fails
776+ */
777+ async function retrieveOAuthMetadataFromAuthorizationServer (
778+ authorizationServerUrl : string | URL ,
779+ {
780+ fetchFn = fetch ,
781+ protocolVersion,
782+ } : {
783+ fetchFn ?: FetchLike ;
784+ protocolVersion : string ;
785+ }
786+ ) : Promise < OAuthMetadata | undefined > {
787+ const url = typeof authorizationServerUrl === 'string' ? new URL ( authorizationServerUrl ) : authorizationServerUrl ;
788+
789+ const hasPath = url . pathname !== '/' ;
790+
791+ const metadataEndpoint = new URL (
792+ buildWellKnownPath ( 'oauth-authorization-server' , hasPath ? url . pathname : '' ) ,
793+ url . origin
794+ ) ;
795+
796+ const response = await fetchWithCorsRetry ( metadataEndpoint , getProtocolVersionHeader ( protocolVersion ) , fetchFn ) ;
797+
798+ if ( ! response ) {
799+ throw new Error ( `CORS error trying to load OAuth metadata from ${ metadataEndpoint } ` ) ;
800+ }
801+
802+ if ( ! response . ok ) {
803+ if ( response . status === 404 ) {
804+ return undefined ;
805+ }
806+
807+ throw new Error ( `HTTP ${ response . status } trying to load OAuth metadata from ${ metadataEndpoint } ` ) ;
808+ }
809+
810+ return OAuthMetadataSchema . parse ( await response . json ( ) ) ;
811+ }
812+
813+ /**
814+ * Retrieves OpenID Connect Discovery 1.0 metadata from the authorization server.
815+ *
816+ * @param authorizationServerUrl - The authorization server URL
817+ * @param options - Configuration options
818+ * @param options.fetchFn - Optional fetch function for making HTTP requests, defaults to global fetch
819+ * @param options.protocolVersion - MCP protocol version to use (required)
820+ * @returns Promise resolving to OpenID provider metadata, or undefined if discovery fails
821+ */
822+ async function retrieveOpenIdProviderMetadataFromAuthorizationServer (
823+ authorizationServerUrl : string | URL ,
824+ {
825+ fetchFn = fetch ,
826+ protocolVersion,
827+ } : {
828+ fetchFn ?: FetchLike ;
829+ protocolVersion : string ;
830+ }
831+ ) : Promise < OpenIdProviderDiscoveryMetadata | undefined > {
832+ const url = typeof authorizationServerUrl === 'string' ? new URL ( authorizationServerUrl ) : authorizationServerUrl ;
833+ const hasPath = url . pathname !== '/' ;
834+
835+ const potentialMetadataEndpoints = hasPath
836+ ? [
837+ // https://example.com/.well-known/openid-configuration/tenant1
838+ new URL ( buildWellKnownPath ( 'openid-configuration' , url . pathname ) , url . origin ) ,
839+ // https://example.com/tenant1/.well-known/openid-configuration
840+ new URL ( buildWellKnownPath ( 'openid-configuration' , url . pathname , { prependPathname : true } ) , `${ url . origin } ` ) ,
841+ ]
842+ : [
843+ // https://example.com/.well-known/openid-configuration
844+ new URL ( buildWellKnownPath ( 'openid-configuration' ) , url . origin ) ,
845+ ] ;
846+
847+ for ( const endpoint of potentialMetadataEndpoints ) {
848+ const response = await fetchWithCorsRetry ( endpoint , getProtocolVersionHeader ( protocolVersion ) , fetchFn ) ;
849+
850+ if ( ! response ) {
851+ throw new Error ( `CORS error trying to load OpenID provider metadata from ${ endpoint } ` ) ;
852+ }
853+
854+ if ( ! response . ok ) {
855+ if ( response . status === 404 ) {
856+ continue ;
857+ }
858+
859+ throw new Error ( `HTTP ${ response . status } trying to load OpenID provider metadata from ${ endpoint } ` ) ;
860+ }
861+
862+ return OpenIdProviderDiscoveryMetadataSchema . parse ( await response . json ( ) ) ;
863+ }
864+
865+ return undefined ;
866+ }
867+
868+ function getProtocolVersionHeader ( protocolVersion : string ) : Record < string , string > {
869+ return {
870+ 'MCP-Protocol-Version' : protocolVersion ,
871+ } ;
872+ }
873+
643874/**
644875 * Begins the authorization flow with the given server, by generating a PKCE challenge and constructing the authorization URL.
645876 */
@@ -653,7 +884,7 @@ export async function startAuthorization(
653884 state,
654885 resource,
655886 } : {
656- metadata ?: OAuthMetadata ;
887+ metadata ?: AuthorizationServerMetadata ;
657888 clientInformation : OAuthClientInformation ;
658889 redirectUrl : string | URL ;
659890 scope ?: string ;
@@ -746,7 +977,7 @@ export async function exchangeAuthorization(
746977 addClientAuthentication,
747978 fetchFn,
748979 } : {
749- metadata ?: OAuthMetadata ;
980+ metadata ?: AuthorizationServerMetadata ;
750981 clientInformation : OAuthClientInformation ;
751982 authorizationCode : string ;
752983 codeVerifier : string ;
@@ -831,7 +1062,7 @@ export async function refreshAuthorization(
8311062 addClientAuthentication,
8321063 fetchFn,
8331064 } : {
834- metadata ?: OAuthMetadata ;
1065+ metadata ?: AuthorizationServerMetadata ;
8351066 clientInformation : OAuthClientInformation ;
8361067 refreshToken : string ;
8371068 resource ?: URL ;
@@ -902,7 +1133,7 @@ export async function registerClient(
9021133 clientMetadata,
9031134 fetchFn,
9041135 } : {
905- metadata ?: OAuthMetadata ;
1136+ metadata ?: AuthorizationServerMetadata ;
9061137 clientMetadata : OAuthClientMetadata ;
9071138 fetchFn ?: FetchLike ;
9081139 } ,
0 commit comments