@@ -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,219 @@ 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+ code_challenge_methods_supported : [ 'S256' ] ,
760+ } ;
761+ }
762+
763+ throw new Error ( `HTTP ${ response . status } trying to load OAuth metadata from ${ metadataEndpoint } ` ) ;
764+ }
765+
766+ return OAuthMetadataSchema . parse ( await response . json ( ) ) ;
767+ }
768+
769+ /**
770+ * Retrieves RFC 8414 OAuth 2.0 Authorization Server Metadata from the authorization server.
771+ *
772+ * @param authorizationServerUrl - The authorization server URL
773+ * @param options - Configuration options
774+ * @param options.fetchFn - Optional fetch function for making HTTP requests, defaults to global fetch
775+ * @param options.protocolVersion - MCP protocol version to use (required)
776+ * @returns Promise resolving to OAuth metadata, or undefined if discovery fails
777+ */
778+ async function retrieveOAuthMetadataFromAuthorizationServer (
779+ authorizationServerUrl : string | URL ,
780+ {
781+ fetchFn = fetch ,
782+ protocolVersion,
783+ } : {
784+ fetchFn ?: FetchLike ;
785+ protocolVersion : string ;
786+ }
787+ ) : Promise < OAuthMetadata | undefined > {
788+ const url = typeof authorizationServerUrl === 'string' ? new URL ( authorizationServerUrl ) : authorizationServerUrl ;
789+
790+ const hasPath = url . pathname !== '/' ;
791+
792+ const metadataEndpoint = new URL (
793+ buildWellKnownPath ( 'oauth-authorization-server' , hasPath ? url . pathname : '' ) ,
794+ url . origin
795+ ) ;
796+
797+ const response = await fetchWithCorsRetry ( metadataEndpoint , getProtocolVersionHeader ( protocolVersion ) , fetchFn ) ;
798+
799+ if ( ! response ) {
800+ throw new Error ( `CORS error trying to load OAuth metadata from ${ metadataEndpoint } ` ) ;
801+ }
802+
803+ if ( ! response . ok ) {
804+ if ( response . status === 404 ) {
805+ return undefined ;
806+ }
807+
808+ throw new Error ( `HTTP ${ response . status } trying to load OAuth metadata from ${ metadataEndpoint } ` ) ;
809+ }
810+
811+ return OAuthMetadataSchema . parse ( await response . json ( ) ) ;
812+ }
813+
814+ /**
815+ * Retrieves OpenID Connect Discovery 1.0 metadata from the authorization server.
816+ *
817+ * @param authorizationServerUrl - The authorization server URL
818+ * @param options - Configuration options
819+ * @param options.fetchFn - Optional fetch function for making HTTP requests, defaults to global fetch
820+ * @param options.protocolVersion - MCP protocol version to use (required)
821+ * @returns Promise resolving to OpenID provider metadata, or undefined if discovery fails
822+ */
823+ async function retrieveOpenIdProviderMetadataFromAuthorizationServer (
824+ authorizationServerUrl : string | URL ,
825+ {
826+ fetchFn = fetch ,
827+ protocolVersion,
828+ } : {
829+ fetchFn ?: FetchLike ;
830+ protocolVersion : string ;
831+ }
832+ ) : Promise < OpenIdProviderDiscoveryMetadata | undefined > {
833+ const url = typeof authorizationServerUrl === 'string' ? new URL ( authorizationServerUrl ) : authorizationServerUrl ;
834+ const hasPath = url . pathname !== '/' ;
835+
836+ const potentialMetadataEndpoints = hasPath
837+ ? [
838+ // https://example.com/.well-known/openid-configuration/tenant1
839+ new URL ( buildWellKnownPath ( 'openid-configuration' , url . pathname ) , url . origin ) ,
840+ // https://example.com/tenant1/.well-known/openid-configuration
841+ new URL ( buildWellKnownPath ( 'openid-configuration' , url . pathname , { prependPathname : true } ) , `${ url . origin } ` ) ,
842+ ]
843+ : [
844+ // https://example.com/.well-known/openid-configuration
845+ new URL ( buildWellKnownPath ( 'openid-configuration' ) , url . origin ) ,
846+ ] ;
847+
848+ for ( const endpoint of potentialMetadataEndpoints ) {
849+ const response = await fetchWithCorsRetry ( endpoint , getProtocolVersionHeader ( protocolVersion ) , fetchFn ) ;
850+
851+ if ( ! response ) {
852+ throw new Error ( `CORS error trying to load OpenID provider metadata from ${ endpoint } ` ) ;
853+ }
854+
855+ if ( ! response . ok ) {
856+ if ( response . status === 404 ) {
857+ continue ;
858+ }
859+
860+ throw new Error ( `HTTP ${ response . status } trying to load OpenID provider metadata from ${ endpoint } ` ) ;
861+ }
862+
863+ return OpenIdProviderDiscoveryMetadataSchema . parse ( await response . json ( ) ) ;
864+ }
865+
866+ return undefined ;
867+ }
868+
869+ function getProtocolVersionHeader ( protocolVersion : string ) : Record < string , string > {
870+ return {
871+ 'MCP-Protocol-Version' : protocolVersion ,
872+ } ;
873+ }
874+
643875/**
644876 * Begins the authorization flow with the given server, by generating a PKCE challenge and constructing the authorization URL.
645877 */
@@ -653,7 +885,7 @@ export async function startAuthorization(
653885 state,
654886 resource,
655887 } : {
656- metadata ?: OAuthMetadata ;
888+ metadata ?: AuthorizationServerMetadata ;
657889 clientInformation : OAuthClientInformation ;
658890 redirectUrl : string | URL ;
659891 scope ?: string ;
@@ -746,7 +978,7 @@ export async function exchangeAuthorization(
746978 addClientAuthentication,
747979 fetchFn,
748980 } : {
749- metadata ?: OAuthMetadata ;
981+ metadata ?: AuthorizationServerMetadata ;
750982 clientInformation : OAuthClientInformation ;
751983 authorizationCode : string ;
752984 codeVerifier : string ;
@@ -831,7 +1063,7 @@ export async function refreshAuthorization(
8311063 addClientAuthentication,
8321064 fetchFn,
8331065 } : {
834- metadata ?: OAuthMetadata ;
1066+ metadata ?: AuthorizationServerMetadata ;
8351067 clientInformation : OAuthClientInformation ;
8361068 refreshToken : string ;
8371069 resource ?: URL ;
@@ -902,7 +1134,7 @@ export async function registerClient(
9021134 clientMetadata,
9031135 fetchFn,
9041136 } : {
905- metadata ?: OAuthMetadata ;
1137+ metadata ?: AuthorizationServerMetadata ;
9061138 clientMetadata : OAuthClientMetadata ;
9071139 fetchFn ?: FetchLike ;
9081140 } ,
0 commit comments