@@ -954,10 +954,19 @@ describe("OAuth Authorization", () => {
954
954
} ) ;
955
955
956
956
it ( "passes resource parameter through authorization flow" , async ( ) => {
957
- // Mock successful metadata discovery
957
+ // Mock successful metadata discovery - need to include protected resource metadata
958
958
mockFetch . mockImplementation ( ( url ) => {
959
959
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" ) ) {
961
970
return Promise . resolve ( {
962
971
ok : true ,
963
972
status : 200 ,
@@ -1002,11 +1011,20 @@ describe("OAuth Authorization", () => {
1002
1011
} ) ;
1003
1012
1004
1013
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
1006
1015
mockFetch . mockImplementation ( ( url ) => {
1007
1016
const urlString = url . toString ( ) ;
1008
1017
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" ) ) {
1010
1028
return Promise . resolve ( {
1011
1029
ok : true ,
1012
1030
status : 200 ,
@@ -1062,11 +1080,20 @@ describe("OAuth Authorization", () => {
1062
1080
} ) ;
1063
1081
1064
1082
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
1066
1084
mockFetch . mockImplementation ( ( url ) => {
1067
1085
const urlString = url . toString ( ) ;
1068
1086
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" ) ) {
1070
1097
return Promise . resolve ( {
1071
1098
ok : true ,
1072
1099
status : 200 ,
@@ -1244,5 +1271,197 @@ describe("OAuth Authorization", () => {
1244
1271
// Should use the PRM's resource value, not the full requested URL
1245
1272
expect ( authUrl . searchParams . get ( "resource" ) ) . toBe ( "https://api.example.com/" ) ;
1246
1273
} ) ;
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
+ } ) ;
1247
1466
} ) ;
1248
1467
} ) ;
0 commit comments