@@ -954,10 +954,19 @@ describe("OAuth Authorization", () => {
954954 } ) ;
955955
956956 it ( "passes resource parameter through authorization flow" , async ( ) => {
957- // Mock successful metadata discovery
957+ // Mock successful metadata discovery - need to include protected resource metadata
958958 mockFetch . mockImplementation ( ( url ) => {
959959 const urlString = url . toString ( ) ;
960- if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
960+ if ( urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
961+ return Promise . resolve ( {
962+ ok : true ,
963+ status : 200 ,
964+ json : async ( ) => ( {
965+ resource : "https://api.example.com/mcp-server" ,
966+ authorization_servers : [ "https://auth.example.com" ] ,
967+ } ) ,
968+ } ) ;
969+ } else if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
961970 return Promise . resolve ( {
962971 ok : true ,
963972 status : 200 ,
@@ -1002,11 +1011,20 @@ describe("OAuth Authorization", () => {
10021011 } ) ;
10031012
10041013 it ( "includes resource in token exchange when authorization code is provided" , async ( ) => {
1005- // Mock successful metadata discovery and token exchange
1014+ // Mock successful metadata discovery and token exchange - need protected resource metadata
10061015 mockFetch . mockImplementation ( ( url ) => {
10071016 const urlString = url . toString ( ) ;
10081017
1009- if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
1018+ if ( urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
1019+ return Promise . resolve ( {
1020+ ok : true ,
1021+ status : 200 ,
1022+ json : async ( ) => ( {
1023+ resource : "https://api.example.com/mcp-server" ,
1024+ authorization_servers : [ "https://auth.example.com" ] ,
1025+ } ) ,
1026+ } ) ;
1027+ } else if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
10101028 return Promise . resolve ( {
10111029 ok : true ,
10121030 status : 200 ,
@@ -1062,11 +1080,20 @@ describe("OAuth Authorization", () => {
10621080 } ) ;
10631081
10641082 it ( "includes resource in token refresh" , async ( ) => {
1065- // Mock successful metadata discovery and token refresh
1083+ // Mock successful metadata discovery and token refresh - need protected resource metadata
10661084 mockFetch . mockImplementation ( ( url ) => {
10671085 const urlString = url . toString ( ) ;
10681086
1069- if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
1087+ if ( urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
1088+ return Promise . resolve ( {
1089+ ok : true ,
1090+ status : 200 ,
1091+ json : async ( ) => ( {
1092+ resource : "https://api.example.com/mcp-server" ,
1093+ authorization_servers : [ "https://auth.example.com" ] ,
1094+ } ) ,
1095+ } ) ;
1096+ } else if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
10701097 return Promise . resolve ( {
10711098 ok : true ,
10721099 status : 200 ,
@@ -1244,5 +1271,197 @@ describe("OAuth Authorization", () => {
12441271 // Should use the PRM's resource value, not the full requested URL
12451272 expect ( authUrl . searchParams . get ( "resource" ) ) . toBe ( "https://api.example.com/" ) ;
12461273 } ) ;
1274+
1275+ it ( "excludes resource parameter when Protected Resource Metadata is not present" , async ( ) => {
1276+ // Mock metadata discovery where protected resource metadata is not available (404)
1277+ // but authorization server metadata is available
1278+ mockFetch . mockImplementation ( ( url ) => {
1279+ const urlString = url . toString ( ) ;
1280+
1281+ if ( urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
1282+ // Protected resource metadata not available
1283+ return Promise . resolve ( {
1284+ ok : false ,
1285+ status : 404 ,
1286+ } ) ;
1287+ } else if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
1288+ return Promise . resolve ( {
1289+ ok : true ,
1290+ status : 200 ,
1291+ json : async ( ) => ( {
1292+ issuer : "https://auth.example.com" ,
1293+ authorization_endpoint : "https://auth.example.com/authorize" ,
1294+ token_endpoint : "https://auth.example.com/token" ,
1295+ response_types_supported : [ "code" ] ,
1296+ code_challenge_methods_supported : [ "S256" ] ,
1297+ } ) ,
1298+ } ) ;
1299+ }
1300+
1301+ return Promise . resolve ( { ok : false , status : 404 } ) ;
1302+ } ) ;
1303+
1304+ // Mock provider methods
1305+ ( mockProvider . clientInformation as jest . Mock ) . mockResolvedValue ( {
1306+ client_id : "test-client" ,
1307+ client_secret : "test-secret" ,
1308+ } ) ;
1309+ ( mockProvider . tokens as jest . Mock ) . mockResolvedValue ( undefined ) ;
1310+ ( mockProvider . saveCodeVerifier as jest . Mock ) . mockResolvedValue ( undefined ) ;
1311+ ( mockProvider . redirectToAuthorization as jest . Mock ) . mockResolvedValue ( undefined ) ;
1312+
1313+ // Call auth - should not include resource parameter
1314+ const result = await auth ( mockProvider , {
1315+ serverUrl : "https://api.example.com/mcp-server" ,
1316+ } ) ;
1317+
1318+ expect ( result ) . toBe ( "REDIRECT" ) ;
1319+
1320+ // Verify the authorization URL does NOT include the resource parameter
1321+ expect ( mockProvider . redirectToAuthorization ) . toHaveBeenCalledWith (
1322+ expect . objectContaining ( {
1323+ searchParams : expect . any ( URLSearchParams ) ,
1324+ } )
1325+ ) ;
1326+
1327+ const redirectCall = ( mockProvider . redirectToAuthorization as jest . Mock ) . mock . calls [ 0 ] ;
1328+ const authUrl : URL = redirectCall [ 0 ] ;
1329+ // Resource parameter should not be present when PRM is not available
1330+ expect ( authUrl . searchParams . has ( "resource" ) ) . toBe ( false ) ;
1331+ } ) ;
1332+
1333+ it ( "excludes resource parameter in token exchange when Protected Resource Metadata is not present" , async ( ) => {
1334+ // Mock metadata discovery - no protected resource metadata, but auth server metadata available
1335+ mockFetch . mockImplementation ( ( url ) => {
1336+ const urlString = url . toString ( ) ;
1337+
1338+ if ( urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
1339+ return Promise . resolve ( {
1340+ ok : false ,
1341+ status : 404 ,
1342+ } ) ;
1343+ } else if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
1344+ return Promise . resolve ( {
1345+ ok : true ,
1346+ status : 200 ,
1347+ json : async ( ) => ( {
1348+ issuer : "https://auth.example.com" ,
1349+ authorization_endpoint : "https://auth.example.com/authorize" ,
1350+ token_endpoint : "https://auth.example.com/token" ,
1351+ response_types_supported : [ "code" ] ,
1352+ code_challenge_methods_supported : [ "S256" ] ,
1353+ } ) ,
1354+ } ) ;
1355+ } else if ( urlString . includes ( "/token" ) ) {
1356+ return Promise . resolve ( {
1357+ ok : true ,
1358+ status : 200 ,
1359+ json : async ( ) => ( {
1360+ access_token : "access123" ,
1361+ token_type : "Bearer" ,
1362+ expires_in : 3600 ,
1363+ refresh_token : "refresh123" ,
1364+ } ) ,
1365+ } ) ;
1366+ }
1367+
1368+ return Promise . resolve ( { ok : false , status : 404 } ) ;
1369+ } ) ;
1370+
1371+ // Mock provider methods for token exchange
1372+ ( mockProvider . clientInformation as jest . Mock ) . mockResolvedValue ( {
1373+ client_id : "test-client" ,
1374+ client_secret : "test-secret" ,
1375+ } ) ;
1376+ ( mockProvider . codeVerifier as jest . Mock ) . mockResolvedValue ( "test-verifier" ) ;
1377+ ( mockProvider . saveTokens as jest . Mock ) . mockResolvedValue ( undefined ) ;
1378+
1379+ // Call auth with authorization code
1380+ const result = await auth ( mockProvider , {
1381+ serverUrl : "https://api.example.com/mcp-server" ,
1382+ authorizationCode : "auth-code-123" ,
1383+ } ) ;
1384+
1385+ expect ( result ) . toBe ( "AUTHORIZED" ) ;
1386+
1387+ // Find the token exchange call
1388+ const tokenCall = mockFetch . mock . calls . find ( call =>
1389+ call [ 0 ] . toString ( ) . includes ( "/token" )
1390+ ) ;
1391+ expect ( tokenCall ) . toBeDefined ( ) ;
1392+
1393+ const body = tokenCall ! [ 1 ] . body as URLSearchParams ;
1394+ // Resource parameter should not be present when PRM is not available
1395+ expect ( body . has ( "resource" ) ) . toBe ( false ) ;
1396+ expect ( body . get ( "code" ) ) . toBe ( "auth-code-123" ) ;
1397+ } ) ;
1398+
1399+ it ( "excludes resource parameter in token refresh when Protected Resource Metadata is not present" , async ( ) => {
1400+ // Mock metadata discovery - no protected resource metadata, but auth server metadata available
1401+ mockFetch . mockImplementation ( ( url ) => {
1402+ const urlString = url . toString ( ) ;
1403+
1404+ if ( urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
1405+ return Promise . resolve ( {
1406+ ok : false ,
1407+ status : 404 ,
1408+ } ) ;
1409+ } else if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
1410+ return Promise . resolve ( {
1411+ ok : true ,
1412+ status : 200 ,
1413+ json : async ( ) => ( {
1414+ issuer : "https://auth.example.com" ,
1415+ authorization_endpoint : "https://auth.example.com/authorize" ,
1416+ token_endpoint : "https://auth.example.com/token" ,
1417+ response_types_supported : [ "code" ] ,
1418+ code_challenge_methods_supported : [ "S256" ] ,
1419+ } ) ,
1420+ } ) ;
1421+ } else if ( urlString . includes ( "/token" ) ) {
1422+ return Promise . resolve ( {
1423+ ok : true ,
1424+ status : 200 ,
1425+ json : async ( ) => ( {
1426+ access_token : "new-access123" ,
1427+ token_type : "Bearer" ,
1428+ expires_in : 3600 ,
1429+ } ) ,
1430+ } ) ;
1431+ }
1432+
1433+ return Promise . resolve ( { ok : false , status : 404 } ) ;
1434+ } ) ;
1435+
1436+ // Mock provider methods for token refresh
1437+ ( mockProvider . clientInformation as jest . Mock ) . mockResolvedValue ( {
1438+ client_id : "test-client" ,
1439+ client_secret : "test-secret" ,
1440+ } ) ;
1441+ ( mockProvider . tokens as jest . Mock ) . mockResolvedValue ( {
1442+ access_token : "old-access" ,
1443+ refresh_token : "refresh123" ,
1444+ } ) ;
1445+ ( mockProvider . saveTokens as jest . Mock ) . mockResolvedValue ( undefined ) ;
1446+
1447+ // Call auth with existing tokens (should trigger refresh)
1448+ const result = await auth ( mockProvider , {
1449+ serverUrl : "https://api.example.com/mcp-server" ,
1450+ } ) ;
1451+
1452+ expect ( result ) . toBe ( "AUTHORIZED" ) ;
1453+
1454+ // Find the token refresh call
1455+ const tokenCall = mockFetch . mock . calls . find ( call =>
1456+ call [ 0 ] . toString ( ) . includes ( "/token" )
1457+ ) ;
1458+ expect ( tokenCall ) . toBeDefined ( ) ;
1459+
1460+ const body = tokenCall ! [ 1 ] . body as URLSearchParams ;
1461+ // Resource parameter should not be present when PRM is not available
1462+ expect ( body . has ( "resource" ) ) . toBe ( false ) ;
1463+ expect ( body . get ( "grant_type" ) ) . toBe ( "refresh_token" ) ;
1464+ expect ( body . get ( "refresh_token" ) ) . toBe ( "refresh123" ) ;
1465+ } ) ;
12471466 } ) ;
12481467} ) ;
0 commit comments