@@ -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" , ( ) => {
@@ -619,6 +648,39 @@ describe("OAuth Authorization", () => {
619
648
discoverOAuthMetadata ( "https://auth.example.com" )
620
649
) . rejects . toThrow ( ) ;
621
650
} ) ;
651
+
652
+ it ( "supports overriding the fetch function used for requests" , async ( ) => {
653
+ const validMetadata = {
654
+ issuer : "https://auth.example.com" ,
655
+ authorization_endpoint : "https://auth.example.com/authorize" ,
656
+ token_endpoint : "https://auth.example.com/token" ,
657
+ registration_endpoint : "https://auth.example.com/register" ,
658
+ response_types_supported : [ "code" ] ,
659
+ code_challenge_methods_supported : [ "S256" ] ,
660
+ } ;
661
+
662
+ const customFetch = jest . fn ( ) . mockResolvedValue ( {
663
+ ok : true ,
664
+ status : 200 ,
665
+ json : async ( ) => validMetadata ,
666
+ } ) ;
667
+
668
+ const metadata = await discoverOAuthMetadata (
669
+ "https://auth.example.com" ,
670
+ { } ,
671
+ customFetch
672
+ ) ;
673
+
674
+ expect ( metadata ) . toEqual ( validMetadata ) ;
675
+ expect ( customFetch ) . toHaveBeenCalledTimes ( 1 ) ;
676
+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
677
+
678
+ const [ url , options ] = customFetch . mock . calls [ 0 ] ;
679
+ expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
680
+ expect ( options . headers ) . toEqual ( {
681
+ "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
682
+ } ) ;
683
+ } ) ;
622
684
} ) ;
623
685
624
686
describe ( "startAuthorization" , ( ) => {
@@ -917,6 +979,46 @@ describe("OAuth Authorization", () => {
917
979
} )
918
980
) . rejects . toThrow ( "Token exchange failed" ) ;
919
981
} ) ;
982
+
983
+ it ( "supports overriding the fetch function used for requests" , async ( ) => {
984
+ const customFetch = jest . fn ( ) . mockResolvedValue ( {
985
+ ok : true ,
986
+ status : 200 ,
987
+ json : async ( ) => validTokens ,
988
+ } ) ;
989
+
990
+ const tokens = await exchangeAuthorization ( "https://auth.example.com" , {
991
+ clientInformation : validClientInfo ,
992
+ authorizationCode : "code123" ,
993
+ codeVerifier : "verifier123" ,
994
+ redirectUri : "http://localhost:3000/callback" ,
995
+ resource : new URL ( "https://api.example.com/mcp-server" ) ,
996
+ fetchFn : customFetch ,
997
+ } ) ;
998
+
999
+ expect ( tokens ) . toEqual ( validTokens ) ;
1000
+ expect ( customFetch ) . toHaveBeenCalledTimes ( 1 ) ;
1001
+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
1002
+
1003
+ const [ url , options ] = customFetch . mock . calls [ 0 ] ;
1004
+ expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/token" ) ;
1005
+ expect ( options ) . toEqual (
1006
+ expect . objectContaining ( {
1007
+ method : "POST" ,
1008
+ headers : expect . any ( Headers ) ,
1009
+ body : expect . any ( URLSearchParams ) ,
1010
+ } )
1011
+ ) ;
1012
+
1013
+ const body = options . body as URLSearchParams ;
1014
+ expect ( body . get ( "grant_type" ) ) . toBe ( "authorization_code" ) ;
1015
+ expect ( body . get ( "code" ) ) . toBe ( "code123" ) ;
1016
+ expect ( body . get ( "code_verifier" ) ) . toBe ( "verifier123" ) ;
1017
+ expect ( body . get ( "client_id" ) ) . toBe ( "client123" ) ;
1018
+ expect ( body . get ( "client_secret" ) ) . toBe ( "secret123" ) ;
1019
+ expect ( body . get ( "redirect_uri" ) ) . toBe ( "http://localhost:3000/callback" ) ;
1020
+ expect ( body . get ( "resource" ) ) . toBe ( "https://api.example.com/mcp-server" ) ;
1021
+ } ) ;
920
1022
} ) ;
921
1023
922
1024
describe ( "refreshAuthorization" , ( ) => {
@@ -1824,6 +1926,68 @@ describe("OAuth Authorization", () => {
1824
1926
// Second call should be to AS metadata with the path from authorization server
1825
1927
expect ( calls [ 1 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server/oauth" ) ;
1826
1928
} ) ;
1929
+
1930
+ it ( "supports overriding the fetch function used for requests" , async ( ) => {
1931
+ const customFetch = jest . fn ( ) ;
1932
+
1933
+ // Mock PRM discovery
1934
+ customFetch . mockResolvedValueOnce ( {
1935
+ ok : true ,
1936
+ status : 200 ,
1937
+ json : async ( ) => ( {
1938
+ resource : "https://resource.example.com" ,
1939
+ authorization_servers : [ "https://auth.example.com" ] ,
1940
+ } ) ,
1941
+ } ) ;
1942
+
1943
+ // Mock AS metadata discovery
1944
+ customFetch . mockResolvedValueOnce ( {
1945
+ ok : true ,
1946
+ status : 200 ,
1947
+ json : async ( ) => ( {
1948
+ issuer : "https://auth.example.com" ,
1949
+ authorization_endpoint : "https://auth.example.com/authorize" ,
1950
+ token_endpoint : "https://auth.example.com/token" ,
1951
+ registration_endpoint : "https://auth.example.com/register" ,
1952
+ response_types_supported : [ "code" ] ,
1953
+ code_challenge_methods_supported : [ "S256" ] ,
1954
+ } ) ,
1955
+ } ) ;
1956
+
1957
+ const mockProvider : OAuthClientProvider = {
1958
+ get redirectUrl ( ) { return "http://localhost:3000/callback" ; } ,
1959
+ get clientMetadata ( ) {
1960
+ return {
1961
+ client_name : "Test Client" ,
1962
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
1963
+ } ;
1964
+ } ,
1965
+ clientInformation : jest . fn ( ) . mockResolvedValue ( {
1966
+ client_id : "client123" ,
1967
+ client_secret : "secret123" ,
1968
+ } ) ,
1969
+ tokens : jest . fn ( ) . mockResolvedValue ( undefined ) ,
1970
+ saveTokens : jest . fn ( ) ,
1971
+ redirectToAuthorization : jest . fn ( ) ,
1972
+ saveCodeVerifier : jest . fn ( ) ,
1973
+ codeVerifier : jest . fn ( ) . mockResolvedValue ( "verifier123" ) ,
1974
+ } ;
1975
+
1976
+ const result = await auth ( mockProvider , {
1977
+ serverUrl : "https://resource.example.com" ,
1978
+ fetchFn : customFetch ,
1979
+ } ) ;
1980
+
1981
+ expect ( result ) . toBe ( "REDIRECT" ) ;
1982
+ expect ( customFetch ) . toHaveBeenCalledTimes ( 2 ) ;
1983
+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
1984
+
1985
+ // Verify custom fetch was called for PRM discovery
1986
+ expect ( customFetch . mock . calls [ 0 ] [ 0 ] . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource" ) ;
1987
+
1988
+ // Verify custom fetch was called for AS metadata discovery
1989
+ expect ( customFetch . mock . calls [ 1 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
1990
+ } ) ;
1827
1991
} ) ;
1828
1992
1829
1993
describe ( "exchangeAuthorization with multiple client authentication methods" , ( ) => {
0 commit comments