@@ -325,6 +325,7 @@ describe("OAuth Authorization", () => {
325325 metadata : undefined ,
326326 clientInformation : validClientInfo ,
327327 redirectUrl : "http://localhost:3000/callback" ,
328+ resource : new URL ( "https://api.example.com/mcp-server" ) ,
328329 }
329330 ) ;
330331
@@ -339,6 +340,7 @@ describe("OAuth Authorization", () => {
339340 expect ( authorizationUrl . searchParams . get ( "redirect_uri" ) ) . toBe (
340341 "http://localhost:3000/callback"
341342 ) ;
343+ expect ( authorizationUrl . searchParams . get ( "resource" ) ) . toBe ( "https://api.example.com/mcp-server" ) ;
342344 expect ( codeVerifier ) . toBe ( "test_verifier" ) ;
343345 } ) ;
344346
@@ -466,6 +468,7 @@ describe("OAuth Authorization", () => {
466468 authorizationCode : "code123" ,
467469 codeVerifier : "verifier123" ,
468470 redirectUri : "http://localhost:3000/callback" ,
471+ resource : new URL ( "https://api.example.com/mcp-server" ) ,
469472 } ) ;
470473
471474 expect ( tokens ) . toEqual ( validTokens ) ;
@@ -488,6 +491,7 @@ describe("OAuth Authorization", () => {
488491 expect ( body . get ( "client_id" ) ) . toBe ( "client123" ) ;
489492 expect ( body . get ( "client_secret" ) ) . toBe ( "secret123" ) ;
490493 expect ( body . get ( "redirect_uri" ) ) . toBe ( "http://localhost:3000/callback" ) ;
494+ expect ( body . get ( "resource" ) ) . toBe ( "https://api.example.com/mcp-server" ) ;
491495 } ) ;
492496
493497 it ( "validates token response schema" , async ( ) => {
@@ -555,6 +559,7 @@ describe("OAuth Authorization", () => {
555559 const tokens = await refreshAuthorization ( "https://auth.example.com" , {
556560 clientInformation : validClientInfo ,
557561 refreshToken : "refresh123" ,
562+ resource : new URL ( "https://api.example.com/mcp-server" ) ,
558563 } ) ;
559564
560565 expect ( tokens ) . toEqual ( validTokensWithNewRefreshToken ) ;
@@ -575,6 +580,7 @@ describe("OAuth Authorization", () => {
575580 expect ( body . get ( "refresh_token" ) ) . toBe ( "refresh123" ) ;
576581 expect ( body . get ( "client_id" ) ) . toBe ( "client123" ) ;
577582 expect ( body . get ( "client_secret" ) ) . toBe ( "secret123" ) ;
583+ expect ( body . get ( "resource" ) ) . toBe ( "https://api.example.com/mcp-server" ) ;
578584 } ) ;
579585
580586 it ( "exchanges refresh token for new tokens and keep existing refresh token if none is returned" , async ( ) => {
@@ -808,5 +814,236 @@ describe("OAuth Authorization", () => {
808814 "https://resource.example.com/.well-known/oauth-authorization-server"
809815 ) ;
810816 } ) ;
817+
818+ it ( "passes resource parameter through authorization flow" , async ( ) => {
819+ // Mock successful metadata discovery
820+ mockFetch . mockImplementation ( ( url ) => {
821+ const urlString = url . toString ( ) ;
822+ if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
823+ return Promise . resolve ( {
824+ ok : true ,
825+ status : 200 ,
826+ json : async ( ) => ( {
827+ issuer : "https://auth.example.com" ,
828+ authorization_endpoint : "https://auth.example.com/authorize" ,
829+ token_endpoint : "https://auth.example.com/token" ,
830+ response_types_supported : [ "code" ] ,
831+ code_challenge_methods_supported : [ "S256" ] ,
832+ } ) ,
833+ } ) ;
834+ }
835+ return Promise . resolve ( { ok : false , status : 404 } ) ;
836+ } ) ;
837+
838+ // Mock provider methods for authorization flow
839+ ( mockProvider . clientInformation as jest . Mock ) . mockResolvedValue ( {
840+ client_id : "test-client" ,
841+ client_secret : "test-secret" ,
842+ } ) ;
843+ ( mockProvider . tokens as jest . Mock ) . mockResolvedValue ( undefined ) ;
844+ ( mockProvider . saveCodeVerifier as jest . Mock ) . mockResolvedValue ( undefined ) ;
845+ ( mockProvider . redirectToAuthorization as jest . Mock ) . mockResolvedValue ( undefined ) ;
846+
847+ // Call auth without authorization code (should trigger redirect)
848+ const result = await auth ( mockProvider , {
849+ serverUrl : "https://api.example.com/mcp-server" ,
850+ } ) ;
851+
852+ expect ( result ) . toBe ( "REDIRECT" ) ;
853+
854+ // Verify the authorization URL includes the resource parameter
855+ expect ( mockProvider . redirectToAuthorization ) . toHaveBeenCalledWith (
856+ expect . objectContaining ( {
857+ searchParams : expect . any ( URLSearchParams ) ,
858+ } )
859+ ) ;
860+
861+ const redirectCall = ( mockProvider . redirectToAuthorization as jest . Mock ) . mock . calls [ 0 ] ;
862+ const authUrl : URL = redirectCall [ 0 ] ;
863+ expect ( authUrl . searchParams . get ( "resource" ) ) . toBe ( "https://api.example.com/mcp-server" ) ;
864+ } ) ;
865+
866+ it ( "includes resource in token exchange when authorization code is provided" , async ( ) => {
867+ // Mock successful metadata discovery and token exchange
868+ mockFetch . mockImplementation ( ( url ) => {
869+ const urlString = url . toString ( ) ;
870+
871+ if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
872+ return Promise . resolve ( {
873+ ok : true ,
874+ status : 200 ,
875+ json : async ( ) => ( {
876+ issuer : "https://auth.example.com" ,
877+ authorization_endpoint : "https://auth.example.com/authorize" ,
878+ token_endpoint : "https://auth.example.com/token" ,
879+ response_types_supported : [ "code" ] ,
880+ code_challenge_methods_supported : [ "S256" ] ,
881+ } ) ,
882+ } ) ;
883+ } else if ( urlString . includes ( "/token" ) ) {
884+ return Promise . resolve ( {
885+ ok : true ,
886+ status : 200 ,
887+ json : async ( ) => ( {
888+ access_token : "access123" ,
889+ token_type : "Bearer" ,
890+ expires_in : 3600 ,
891+ refresh_token : "refresh123" ,
892+ } ) ,
893+ } ) ;
894+ }
895+
896+ return Promise . resolve ( { ok : false , status : 404 } ) ;
897+ } ) ;
898+
899+ // Mock provider methods for token exchange
900+ ( mockProvider . clientInformation as jest . Mock ) . mockResolvedValue ( {
901+ client_id : "test-client" ,
902+ client_secret : "test-secret" ,
903+ } ) ;
904+ ( mockProvider . codeVerifier as jest . Mock ) . mockResolvedValue ( "test-verifier" ) ;
905+ ( mockProvider . saveTokens as jest . Mock ) . mockResolvedValue ( undefined ) ;
906+
907+ // Call auth with authorization code
908+ const result = await auth ( mockProvider , {
909+ serverUrl : "https://api.example.com/mcp-server" ,
910+ authorizationCode : "auth-code-123" ,
911+ } ) ;
912+
913+ expect ( result ) . toBe ( "AUTHORIZED" ) ;
914+
915+ // Find the token exchange call
916+ const tokenCall = mockFetch . mock . calls . find ( call =>
917+ call [ 0 ] . toString ( ) . includes ( "/token" )
918+ ) ;
919+ expect ( tokenCall ) . toBeDefined ( ) ;
920+
921+ const body = tokenCall ! [ 1 ] . body as URLSearchParams ;
922+ expect ( body . get ( "resource" ) ) . toBe ( "https://api.example.com/mcp-server" ) ;
923+ expect ( body . get ( "code" ) ) . toBe ( "auth-code-123" ) ;
924+ } ) ;
925+
926+ it ( "includes resource in token refresh" , async ( ) => {
927+ // Mock successful metadata discovery and token refresh
928+ mockFetch . mockImplementation ( ( url ) => {
929+ const urlString = url . toString ( ) ;
930+
931+ if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
932+ return Promise . resolve ( {
933+ ok : true ,
934+ status : 200 ,
935+ json : async ( ) => ( {
936+ issuer : "https://auth.example.com" ,
937+ authorization_endpoint : "https://auth.example.com/authorize" ,
938+ token_endpoint : "https://auth.example.com/token" ,
939+ response_types_supported : [ "code" ] ,
940+ code_challenge_methods_supported : [ "S256" ] ,
941+ } ) ,
942+ } ) ;
943+ } else if ( urlString . includes ( "/token" ) ) {
944+ return Promise . resolve ( {
945+ ok : true ,
946+ status : 200 ,
947+ json : async ( ) => ( {
948+ access_token : "new-access123" ,
949+ token_type : "Bearer" ,
950+ expires_in : 3600 ,
951+ } ) ,
952+ } ) ;
953+ }
954+
955+ return Promise . resolve ( { ok : false , status : 404 } ) ;
956+ } ) ;
957+
958+ // Mock provider methods for token refresh
959+ ( mockProvider . clientInformation as jest . Mock ) . mockResolvedValue ( {
960+ client_id : "test-client" ,
961+ client_secret : "test-secret" ,
962+ } ) ;
963+ ( mockProvider . tokens as jest . Mock ) . mockResolvedValue ( {
964+ access_token : "old-access" ,
965+ refresh_token : "refresh123" ,
966+ } ) ;
967+ ( mockProvider . saveTokens as jest . Mock ) . mockResolvedValue ( undefined ) ;
968+
969+ // Call auth with existing tokens (should trigger refresh)
970+ const result = await auth ( mockProvider , {
971+ serverUrl : "https://api.example.com/mcp-server" ,
972+ } ) ;
973+
974+ expect ( result ) . toBe ( "AUTHORIZED" ) ;
975+
976+ // Find the token refresh call
977+ const tokenCall = mockFetch . mock . calls . find ( call =>
978+ call [ 0 ] . toString ( ) . includes ( "/token" )
979+ ) ;
980+ expect ( tokenCall ) . toBeDefined ( ) ;
981+
982+ const body = tokenCall ! [ 1 ] . body as URLSearchParams ;
983+ expect ( body . get ( "resource" ) ) . toBe ( "https://api.example.com/mcp-server" ) ;
984+ expect ( body . get ( "grant_type" ) ) . toBe ( "refresh_token" ) ;
985+ expect ( body . get ( "refresh_token" ) ) . toBe ( "refresh123" ) ;
986+ } ) ;
987+
988+ it ( "skips default PRM resource validation when custom validateResourceURL is provided" , async ( ) => {
989+ const mockValidateResourceURL = jest . fn ( ) . mockResolvedValue ( undefined ) ;
990+ const providerWithCustomValidation = {
991+ ...mockProvider ,
992+ validateResourceURL : mockValidateResourceURL ,
993+ } ;
994+
995+ // Mock protected resource metadata with mismatched resource URL
996+ // This would normally throw an error in default validation, but should be skipped
997+ mockFetch . mockImplementation ( ( url ) => {
998+ const urlString = url . toString ( ) ;
999+
1000+ if ( urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
1001+ return Promise . resolve ( {
1002+ ok : true ,
1003+ status : 200 ,
1004+ json : async ( ) => ( {
1005+ resource : "https://different-resource.example.com/mcp-server" , // Mismatched resource
1006+ authorization_servers : [ "https://auth.example.com" ] ,
1007+ } ) ,
1008+ } ) ;
1009+ } else if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
1010+ return Promise . resolve ( {
1011+ ok : true ,
1012+ status : 200 ,
1013+ json : async ( ) => ( {
1014+ issuer : "https://auth.example.com" ,
1015+ authorization_endpoint : "https://auth.example.com/authorize" ,
1016+ token_endpoint : "https://auth.example.com/token" ,
1017+ response_types_supported : [ "code" ] ,
1018+ code_challenge_methods_supported : [ "S256" ] ,
1019+ } ) ,
1020+ } ) ;
1021+ }
1022+
1023+ return Promise . resolve ( { ok : false , status : 404 } ) ;
1024+ } ) ;
1025+
1026+ // Mock provider methods
1027+ ( providerWithCustomValidation . clientInformation as jest . Mock ) . mockResolvedValue ( {
1028+ client_id : "test-client" ,
1029+ client_secret : "test-secret" ,
1030+ } ) ;
1031+ ( providerWithCustomValidation . tokens as jest . Mock ) . mockResolvedValue ( undefined ) ;
1032+ ( providerWithCustomValidation . saveCodeVerifier as jest . Mock ) . mockResolvedValue ( undefined ) ;
1033+ ( providerWithCustomValidation . redirectToAuthorization as jest . Mock ) . mockResolvedValue ( undefined ) ;
1034+
1035+ // Call auth - should succeed despite resource mismatch because custom validation overrides default
1036+ const result = await auth ( providerWithCustomValidation , {
1037+ serverUrl : "https://api.example.com/mcp-server" ,
1038+ } ) ;
1039+
1040+ expect ( result ) . toBe ( "REDIRECT" ) ;
1041+
1042+ // Verify custom validation method was called
1043+ expect ( mockValidateResourceURL ) . toHaveBeenCalledWith (
1044+ "https://api.example.com/mcp-server" ,
1045+ "https://different-resource.example.com/mcp-server"
1046+ ) ;
1047+ } ) ;
8111048 } ) ;
8121049} ) ;
0 commit comments