@@ -347,6 +347,35 @@ describe("OAuth Authorization", () => {
347
347
const [ url ] = calls [ 0 ] ;
348
348
expect ( url . toString ( ) ) . toBe ( "https://custom.example.com/metadata" ) ;
349
349
} ) ;
350
+
351
+ it ( "supports overriding the fetch function used for requests" , async ( ) => {
352
+ const validMetadata = {
353
+ resource : "https://resource.example.com" ,
354
+ authorization_servers : [ "https://auth.example.com" ] ,
355
+ } ;
356
+
357
+ const customFetch = jest . fn ( ) . mockResolvedValue ( {
358
+ ok : true ,
359
+ status : 200 ,
360
+ json : async ( ) => validMetadata ,
361
+ } ) ;
362
+
363
+ const metadata = await discoverOAuthProtectedResourceMetadata (
364
+ "https://resource.example.com" ,
365
+ undefined ,
366
+ customFetch
367
+ ) ;
368
+
369
+ expect ( metadata ) . toEqual ( validMetadata ) ;
370
+ expect ( customFetch ) . toHaveBeenCalledTimes ( 1 ) ;
371
+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
372
+
373
+ const [ url , options ] = customFetch . mock . calls [ 0 ] ;
374
+ expect ( url . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource" ) ;
375
+ expect ( options . headers ) . toEqual ( {
376
+ "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
377
+ } ) ;
378
+ } ) ;
350
379
} ) ;
351
380
352
381
describe ( "discoverOAuthMetadata" , ( ) => {
@@ -695,6 +724,39 @@ describe("OAuth Authorization", () => {
695
724
discoverOAuthMetadata ( "https://auth.example.com" )
696
725
) . rejects . toThrow ( ) ;
697
726
} ) ;
727
+
728
+ it ( "supports overriding the fetch function used for requests" , async ( ) => {
729
+ const validMetadata = {
730
+ issuer : "https://auth.example.com" ,
731
+ authorization_endpoint : "https://auth.example.com/authorize" ,
732
+ token_endpoint : "https://auth.example.com/token" ,
733
+ registration_endpoint : "https://auth.example.com/register" ,
734
+ response_types_supported : [ "code" ] ,
735
+ code_challenge_methods_supported : [ "S256" ] ,
736
+ } ;
737
+
738
+ const customFetch = jest . fn ( ) . mockResolvedValue ( {
739
+ ok : true ,
740
+ status : 200 ,
741
+ json : async ( ) => validMetadata ,
742
+ } ) ;
743
+
744
+ const metadata = await discoverOAuthMetadata (
745
+ "https://auth.example.com" ,
746
+ { } ,
747
+ customFetch
748
+ ) ;
749
+
750
+ expect ( metadata ) . toEqual ( validMetadata ) ;
751
+ expect ( customFetch ) . toHaveBeenCalledTimes ( 1 ) ;
752
+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
753
+
754
+ const [ url , options ] = customFetch . mock . calls [ 0 ] ;
755
+ expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
756
+ expect ( options . headers ) . toEqual ( {
757
+ "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
758
+ } ) ;
759
+ } ) ;
698
760
} ) ;
699
761
700
762
describe ( "startAuthorization" , ( ) => {
@@ -993,6 +1055,46 @@ describe("OAuth Authorization", () => {
993
1055
} )
994
1056
) . rejects . toThrow ( "Token exchange failed" ) ;
995
1057
} ) ;
1058
+
1059
+ it ( "supports overriding the fetch function used for requests" , async ( ) => {
1060
+ const customFetch = jest . fn ( ) . mockResolvedValue ( {
1061
+ ok : true ,
1062
+ status : 200 ,
1063
+ json : async ( ) => validTokens ,
1064
+ } ) ;
1065
+
1066
+ const tokens = await exchangeAuthorization ( "https://auth.example.com" , {
1067
+ clientInformation : validClientInfo ,
1068
+ authorizationCode : "code123" ,
1069
+ codeVerifier : "verifier123" ,
1070
+ redirectUri : "http://localhost:3000/callback" ,
1071
+ resource : new URL ( "https://api.example.com/mcp-server" ) ,
1072
+ fetchFn : customFetch ,
1073
+ } ) ;
1074
+
1075
+ expect ( tokens ) . toEqual ( validTokens ) ;
1076
+ expect ( customFetch ) . toHaveBeenCalledTimes ( 1 ) ;
1077
+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
1078
+
1079
+ const [ url , options ] = customFetch . mock . calls [ 0 ] ;
1080
+ expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/token" ) ;
1081
+ expect ( options ) . toEqual (
1082
+ expect . objectContaining ( {
1083
+ method : "POST" ,
1084
+ headers : expect . any ( Headers ) ,
1085
+ body : expect . any ( URLSearchParams ) ,
1086
+ } )
1087
+ ) ;
1088
+
1089
+ const body = options . body as URLSearchParams ;
1090
+ expect ( body . get ( "grant_type" ) ) . toBe ( "authorization_code" ) ;
1091
+ expect ( body . get ( "code" ) ) . toBe ( "code123" ) ;
1092
+ expect ( body . get ( "code_verifier" ) ) . toBe ( "verifier123" ) ;
1093
+ expect ( body . get ( "client_id" ) ) . toBe ( "client123" ) ;
1094
+ expect ( body . get ( "client_secret" ) ) . toBe ( "secret123" ) ;
1095
+ expect ( body . get ( "redirect_uri" ) ) . toBe ( "http://localhost:3000/callback" ) ;
1096
+ expect ( body . get ( "resource" ) ) . toBe ( "https://api.example.com/mcp-server" ) ;
1097
+ } ) ;
996
1098
} ) ;
997
1099
998
1100
describe ( "refreshAuthorization" , ( ) => {
@@ -1900,6 +2002,68 @@ describe("OAuth Authorization", () => {
1900
2002
// Second call should be to AS metadata with the path from authorization server
1901
2003
expect ( calls [ 1 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server/oauth" ) ;
1902
2004
} ) ;
2005
+
2006
+ it ( "supports overriding the fetch function used for requests" , async ( ) => {
2007
+ const customFetch = jest . fn ( ) ;
2008
+
2009
+ // Mock PRM discovery
2010
+ customFetch . mockResolvedValueOnce ( {
2011
+ ok : true ,
2012
+ status : 200 ,
2013
+ json : async ( ) => ( {
2014
+ resource : "https://resource.example.com" ,
2015
+ authorization_servers : [ "https://auth.example.com" ] ,
2016
+ } ) ,
2017
+ } ) ;
2018
+
2019
+ // Mock AS metadata discovery
2020
+ customFetch . mockResolvedValueOnce ( {
2021
+ ok : true ,
2022
+ status : 200 ,
2023
+ json : async ( ) => ( {
2024
+ issuer : "https://auth.example.com" ,
2025
+ authorization_endpoint : "https://auth.example.com/authorize" ,
2026
+ token_endpoint : "https://auth.example.com/token" ,
2027
+ registration_endpoint : "https://auth.example.com/register" ,
2028
+ response_types_supported : [ "code" ] ,
2029
+ code_challenge_methods_supported : [ "S256" ] ,
2030
+ } ) ,
2031
+ } ) ;
2032
+
2033
+ const mockProvider : OAuthClientProvider = {
2034
+ get redirectUrl ( ) { return "http://localhost:3000/callback" ; } ,
2035
+ get clientMetadata ( ) {
2036
+ return {
2037
+ client_name : "Test Client" ,
2038
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
2039
+ } ;
2040
+ } ,
2041
+ clientInformation : jest . fn ( ) . mockResolvedValue ( {
2042
+ client_id : "client123" ,
2043
+ client_secret : "secret123" ,
2044
+ } ) ,
2045
+ tokens : jest . fn ( ) . mockResolvedValue ( undefined ) ,
2046
+ saveTokens : jest . fn ( ) ,
2047
+ redirectToAuthorization : jest . fn ( ) ,
2048
+ saveCodeVerifier : jest . fn ( ) ,
2049
+ codeVerifier : jest . fn ( ) . mockResolvedValue ( "verifier123" ) ,
2050
+ } ;
2051
+
2052
+ const result = await auth ( mockProvider , {
2053
+ serverUrl : "https://resource.example.com" ,
2054
+ fetchFn : customFetch ,
2055
+ } ) ;
2056
+
2057
+ expect ( result ) . toBe ( "REDIRECT" ) ;
2058
+ expect ( customFetch ) . toHaveBeenCalledTimes ( 2 ) ;
2059
+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
2060
+
2061
+ // Verify custom fetch was called for PRM discovery
2062
+ expect ( customFetch . mock . calls [ 0 ] [ 0 ] . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource" ) ;
2063
+
2064
+ // Verify custom fetch was called for AS metadata discovery
2065
+ expect ( customFetch . mock . calls [ 1 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
2066
+ } ) ;
1903
2067
} ) ;
1904
2068
1905
2069
describe ( "exchangeAuthorization with multiple client authentication methods" , ( ) => {
0 commit comments