@@ -7,7 +7,10 @@ import {
7
7
OAuthMetadata ,
8
8
OAuthClientInformationFull ,
9
9
OAuthProtectedResourceMetadata ,
10
- OAuthErrorResponseSchema
10
+ OAuthErrorResponseSchema ,
11
+ OpenIdProviderDiscoveryMetadata ,
12
+ AuthorizationServerMetadata ,
13
+ OpenIdProviderDiscoveryMetadataSchema
11
14
} from "../shared/auth.js" ;
12
15
import { OAuthClientInformationFullSchema , OAuthMetadataSchema , OAuthProtectedResourceMetadataSchema , OAuthTokensSchema } from "../shared/auth.js" ;
13
16
import { checkResourceAllowed , resourceUrlFromServerUrl } from "../shared/auth-utils.js" ;
@@ -108,7 +111,7 @@ export interface OAuthClientProvider {
108
111
* @param url - The token endpoint URL being called
109
112
* @param metadata - Optional OAuth metadata for the server, which may include supported authentication methods
110
113
*/
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 > ;
112
115
113
116
/**
114
117
* If defined, overrides the selection and validation of the
@@ -319,7 +322,7 @@ async function authInternal(
319
322
) : Promise < AuthResult > {
320
323
321
324
let resourceMetadata : OAuthProtectedResourceMetadata | undefined ;
322
- let authorizationServerUrl = serverUrl ;
325
+ let authorizationServerUrl : string | URL | undefined ;
323
326
try {
324
327
resourceMetadata = await discoverOAuthProtectedResourceMetadata ( serverUrl , { resourceMetadataUrl } , fetchFn ) ;
325
328
if ( resourceMetadata . authorization_servers && resourceMetadata . authorization_servers . length > 0 ) {
@@ -331,9 +334,17 @@ async function authInternal(
331
334
332
335
const resource : URL | undefined = await selectResourceURL ( serverUrl , provider , resourceMetadata ) ;
333
336
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
+ }
337
348
338
349
// Handle client registration if needed
339
350
let clientInformation = await Promise . resolve ( provider . clientInformation ( ) ) ;
@@ -524,15 +535,21 @@ async function fetchWithCorsRetry(
524
535
}
525
536
526
537
/**
527
- * Constructs the well-known path for OAuth metadata discovery
538
+ * Constructs the well-known path for auth-related metadata discovery
528
539
*/
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
531
546
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 ) ;
534
548
}
535
- return wellKnownPath ;
549
+
550
+ return options . prependPathname
551
+ ? `${ pathname } /.well-known/${ wellKnownPrefix } `
552
+ : `/.well-known/${ wellKnownPrefix } ${ pathname } ` ;
536
553
}
537
554
538
555
/**
@@ -594,6 +611,8 @@ async function discoverMetadataWithFallback(
594
611
*
595
612
* If the server returns a 404 for the well-known endpoint, this function will
596
613
* return `undefined`. Any other errors will be thrown as exceptions.
614
+ *
615
+ * @deprecated This function is deprecated in favor of `discoverAuthorizationServerMetadata`.
597
616
*/
598
617
export async function discoverOAuthMetadata (
599
618
issuer : string | URL ,
@@ -640,6 +659,218 @@ export async function discoverOAuthMetadata(
640
659
return OAuthMetadataSchema . parse ( await response . json ( ) ) ;
641
660
}
642
661
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
+
643
874
/**
644
875
* Begins the authorization flow with the given server, by generating a PKCE challenge and constructing the authorization URL.
645
876
*/
@@ -653,7 +884,7 @@ export async function startAuthorization(
653
884
state,
654
885
resource,
655
886
} : {
656
- metadata ?: OAuthMetadata ;
887
+ metadata ?: AuthorizationServerMetadata ;
657
888
clientInformation : OAuthClientInformation ;
658
889
redirectUrl : string | URL ;
659
890
scope ?: string ;
@@ -746,7 +977,7 @@ export async function exchangeAuthorization(
746
977
addClientAuthentication,
747
978
fetchFn,
748
979
} : {
749
- metadata ?: OAuthMetadata ;
980
+ metadata ?: AuthorizationServerMetadata ;
750
981
clientInformation : OAuthClientInformation ;
751
982
authorizationCode : string ;
752
983
codeVerifier : string ;
@@ -831,7 +1062,7 @@ export async function refreshAuthorization(
831
1062
addClientAuthentication,
832
1063
fetchFn,
833
1064
} : {
834
- metadata ?: OAuthMetadata ;
1065
+ metadata ?: AuthorizationServerMetadata ;
835
1066
clientInformation : OAuthClientInformation ;
836
1067
refreshToken : string ;
837
1068
resource ?: URL ;
@@ -902,7 +1133,7 @@ export async function registerClient(
902
1133
clientMetadata,
903
1134
fetchFn,
904
1135
} : {
905
- metadata ?: OAuthMetadata ;
1136
+ metadata ?: AuthorizationServerMetadata ;
906
1137
clientMetadata : OAuthClientMetadata ;
907
1138
fetchFn ?: FetchLike ;
908
1139
} ,
0 commit comments