@@ -231,7 +231,7 @@ describe("OAuth Authorization", () => {
231
231
ok : false ,
232
232
status : 404 ,
233
233
} ) ;
234
-
234
+
235
235
// Second call (root fallback) succeeds
236
236
mockFetch . mockResolvedValueOnce ( {
237
237
ok : true ,
@@ -241,17 +241,17 @@ describe("OAuth Authorization", () => {
241
241
242
242
const metadata = await discoverOAuthMetadata ( "https://auth.example.com/path/name" ) ;
243
243
expect ( metadata ) . toEqual ( validMetadata ) ;
244
-
244
+
245
245
const calls = mockFetch . mock . calls ;
246
246
expect ( calls . length ) . toBe ( 2 ) ;
247
-
247
+
248
248
// First call should be path-aware
249
249
const [ firstUrl , firstOptions ] = calls [ 0 ] ;
250
250
expect ( firstUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server/path/name" ) ;
251
251
expect ( firstOptions . headers ) . toEqual ( {
252
252
"MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
253
253
} ) ;
254
-
254
+
255
255
// Second call should be root fallback
256
256
const [ secondUrl , secondOptions ] = calls [ 1 ] ;
257
257
expect ( secondUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
@@ -266,7 +266,7 @@ describe("OAuth Authorization", () => {
266
266
ok : false ,
267
267
status : 404 ,
268
268
} ) ;
269
-
269
+
270
270
// Second call (root fallback) also returns 404
271
271
mockFetch . mockResolvedValueOnce ( {
272
272
ok : false ,
@@ -275,7 +275,7 @@ describe("OAuth Authorization", () => {
275
275
276
276
const metadata = await discoverOAuthMetadata ( "https://auth.example.com/path/name" ) ;
277
277
expect ( metadata ) . toBeUndefined ( ) ;
278
-
278
+
279
279
const calls = mockFetch . mock . calls ;
280
280
expect ( calls . length ) . toBe ( 2 ) ;
281
281
} ) ;
@@ -289,10 +289,10 @@ describe("OAuth Authorization", () => {
289
289
290
290
const metadata = await discoverOAuthMetadata ( "https://auth.example.com/" ) ;
291
291
expect ( metadata ) . toBeUndefined ( ) ;
292
-
292
+
293
293
const calls = mockFetch . mock . calls ;
294
294
expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback
295
-
295
+
296
296
const [ url ] = calls [ 0 ] ;
297
297
expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
298
298
} ) ;
@@ -306,24 +306,24 @@ describe("OAuth Authorization", () => {
306
306
307
307
const metadata = await discoverOAuthMetadata ( "https://auth.example.com" ) ;
308
308
expect ( metadata ) . toBeUndefined ( ) ;
309
-
309
+
310
310
const calls = mockFetch . mock . calls ;
311
311
expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback
312
-
312
+
313
313
const [ url ] = calls [ 0 ] ;
314
314
expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
315
315
} ) ;
316
316
317
317
it ( "falls back when path-aware discovery encounters CORS error" , async ( ) => {
318
318
// First call (path-aware) fails with TypeError (CORS)
319
319
mockFetch . mockImplementationOnce ( ( ) => Promise . reject ( new TypeError ( "CORS error" ) ) ) ;
320
-
320
+
321
321
// Retry path-aware without headers (simulating CORS retry)
322
322
mockFetch . mockResolvedValueOnce ( {
323
323
ok : false ,
324
324
status : 404 ,
325
325
} ) ;
326
-
326
+
327
327
// Second call (root fallback) succeeds
328
328
mockFetch . mockResolvedValueOnce ( {
329
329
ok : true ,
@@ -333,10 +333,10 @@ describe("OAuth Authorization", () => {
333
333
334
334
const metadata = await discoverOAuthMetadata ( "https://auth.example.com/deep/path" ) ;
335
335
expect ( metadata ) . toEqual ( validMetadata ) ;
336
-
336
+
337
337
const calls = mockFetch . mock . calls ;
338
338
expect ( calls . length ) . toBe ( 3 ) ;
339
-
339
+
340
340
// Final call should be root fallback
341
341
const [ lastUrl , lastOptions ] = calls [ 2 ] ;
342
342
expect ( lastUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
@@ -1463,5 +1463,211 @@ describe("OAuth Authorization", () => {
1463
1463
expect ( body . get ( "grant_type" ) ) . toBe ( "refresh_token" ) ;
1464
1464
expect ( body . get ( "refresh_token" ) ) . toBe ( "refresh123" ) ;
1465
1465
} ) ;
1466
+
1467
+ describe ( "delegateAuthorization" , ( ) => {
1468
+ const validMetadata = {
1469
+ issuer : "https://auth.example.com" ,
1470
+ authorization_endpoint : "https://auth.example.com/authorize" ,
1471
+ token_endpoint : "https://auth.example.com/token" ,
1472
+ registration_endpoint : "https://auth.example.com/register" ,
1473
+ response_types_supported : [ "code" ] ,
1474
+ code_challenge_methods_supported : [ "S256" ] ,
1475
+ } ;
1476
+
1477
+ const validClientInfo = {
1478
+ client_id : "client123" ,
1479
+ client_secret : "secret123" ,
1480
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
1481
+ client_name : "Test Client" ,
1482
+ } ;
1483
+
1484
+ const validTokens = {
1485
+ access_token : "access123" ,
1486
+ token_type : "Bearer" ,
1487
+ expires_in : 3600 ,
1488
+ refresh_token : "refresh123" ,
1489
+ } ;
1490
+
1491
+ // Setup shared mock function for all tests
1492
+ beforeEach ( ( ) => {
1493
+ // Reset mockFetch implementation
1494
+ mockFetch . mockReset ( ) ;
1495
+
1496
+ // Set up the mockFetch to respond to all necessary API calls
1497
+ mockFetch . mockImplementation ( ( url ) => {
1498
+ const urlString = url . toString ( ) ;
1499
+
1500
+ if ( urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
1501
+ return Promise . resolve ( {
1502
+ ok : false ,
1503
+ status : 404
1504
+ } ) ;
1505
+ } else if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
1506
+ return Promise . resolve ( {
1507
+ ok : true ,
1508
+ status : 200 ,
1509
+ json : async ( ) => validMetadata
1510
+ } ) ;
1511
+ } else if ( urlString . includes ( "/token" ) ) {
1512
+ return Promise . resolve ( {
1513
+ ok : true ,
1514
+ status : 200 ,
1515
+ json : async ( ) => validTokens
1516
+ } ) ;
1517
+ }
1518
+
1519
+ return Promise . reject ( new Error ( `Unexpected fetch call: ${ urlString } ` ) ) ;
1520
+ } ) ;
1521
+ } ) ;
1522
+
1523
+ it ( "should use delegateAuthorization when implemented and return AUTHORIZED" , async ( ) => {
1524
+ const mockProvider : OAuthClientProvider = {
1525
+ redirectUrl : "http://localhost:3000/callback" ,
1526
+ clientMetadata : {
1527
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
1528
+ client_name : "Test Client"
1529
+ } ,
1530
+ clientInformation : ( ) => validClientInfo ,
1531
+ tokens : ( ) => validTokens ,
1532
+ saveTokens : jest . fn ( ) ,
1533
+ redirectToAuthorization : jest . fn ( ) ,
1534
+ saveCodeVerifier : jest . fn ( ) ,
1535
+ codeVerifier : ( ) => "test_verifier" ,
1536
+ delegateAuthorization : jest . fn ( ) . mockResolvedValue ( "AUTHORIZED" )
1537
+ } ;
1538
+
1539
+ const result = await auth ( mockProvider , { serverUrl : "https://auth.example.com" } ) ;
1540
+
1541
+ expect ( result ) . toBe ( "AUTHORIZED" ) ;
1542
+ expect ( mockProvider . delegateAuthorization ) . toHaveBeenCalledWith (
1543
+ "https://auth.example.com" ,
1544
+ {
1545
+ metadata : expect . objectContaining ( validMetadata ) ,
1546
+ resource : undefined
1547
+ }
1548
+ ) ;
1549
+ expect ( mockProvider . redirectToAuthorization ) . not . toHaveBeenCalled ( ) ;
1550
+ } ) ;
1551
+
1552
+ it ( "should fall back to standard flow when delegateAuthorization returns undefined" , async ( ) => {
1553
+ const mockProvider : OAuthClientProvider = {
1554
+ redirectUrl : "http://localhost:3000/callback" ,
1555
+ clientMetadata : {
1556
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
1557
+ client_name : "Test Client"
1558
+ } ,
1559
+ clientInformation : ( ) => validClientInfo ,
1560
+ tokens : ( ) => validTokens ,
1561
+ saveTokens : jest . fn ( ) ,
1562
+ redirectToAuthorization : jest . fn ( ) ,
1563
+ saveCodeVerifier : jest . fn ( ) ,
1564
+ codeVerifier : ( ) => "test_verifier" ,
1565
+ delegateAuthorization : jest . fn ( ) . mockResolvedValue ( undefined )
1566
+ } ;
1567
+
1568
+ const result = await auth ( mockProvider , { serverUrl : "https://auth.example.com" } ) ;
1569
+
1570
+ expect ( result ) . toBe ( "AUTHORIZED" ) ;
1571
+ expect ( mockProvider . delegateAuthorization ) . toHaveBeenCalled ( ) ;
1572
+ expect ( mockProvider . saveTokens ) . toHaveBeenCalled ( ) ;
1573
+ } ) ;
1574
+
1575
+ it ( "should not call delegateAuthorization when processing authorizationCode" , async ( ) => {
1576
+ const mockProvider : OAuthClientProvider = {
1577
+ redirectUrl : "http://localhost:3000/callback" ,
1578
+ clientMetadata : {
1579
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
1580
+ client_name : "Test Client"
1581
+ } ,
1582
+ clientInformation : ( ) => validClientInfo ,
1583
+ tokens : jest . fn ( ) ,
1584
+ saveTokens : jest . fn ( ) ,
1585
+ redirectToAuthorization : jest . fn ( ) ,
1586
+ saveCodeVerifier : jest . fn ( ) ,
1587
+ codeVerifier : ( ) => "test_verifier" ,
1588
+ delegateAuthorization : jest . fn ( )
1589
+ } ;
1590
+
1591
+ await auth ( mockProvider , {
1592
+ serverUrl : "https://auth.example.com" ,
1593
+ authorizationCode : "code123"
1594
+ } ) ;
1595
+
1596
+ expect ( mockProvider . delegateAuthorization ) . not . toHaveBeenCalled ( ) ;
1597
+ expect ( mockProvider . saveTokens ) . toHaveBeenCalled ( ) ;
1598
+ } ) ;
1599
+
1600
+ it ( "should propagate errors from delegateAuthorization" , async ( ) => {
1601
+ const mockProvider : OAuthClientProvider = {
1602
+ redirectUrl : "http://localhost:3000/callback" ,
1603
+ clientMetadata : {
1604
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
1605
+ client_name : "Test Client"
1606
+ } ,
1607
+ clientInformation : ( ) => validClientInfo ,
1608
+ tokens : jest . fn ( ) ,
1609
+ saveTokens : jest . fn ( ) ,
1610
+ redirectToAuthorization : jest . fn ( ) ,
1611
+ saveCodeVerifier : jest . fn ( ) ,
1612
+ codeVerifier : ( ) => "test_verifier" ,
1613
+ delegateAuthorization : jest . fn ( ) . mockRejectedValue ( new Error ( "Delegation failed" ) )
1614
+ } ;
1615
+
1616
+ await expect ( auth ( mockProvider , { serverUrl : "https://auth.example.com" } ) )
1617
+ . rejects . toThrow ( "Delegation failed" ) ;
1618
+ } ) ;
1619
+
1620
+ it ( "should pass both resource and metadata to delegateAuthorization when available" , async ( ) => {
1621
+ // Mock resource metadata to be returned by the fetch
1622
+ mockFetch . mockImplementation ( ( url ) => {
1623
+ const urlString = url . toString ( ) ;
1624
+
1625
+ if ( urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
1626
+ return Promise . resolve ( {
1627
+ ok : true ,
1628
+ status : 200 ,
1629
+ json : async ( ) => ( {
1630
+ resource : "https://api.example.com/" ,
1631
+ authorization_servers : [ "https://auth.example.com" ]
1632
+ } )
1633
+ } ) ;
1634
+ } else if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
1635
+ return Promise . resolve ( {
1636
+ ok : true ,
1637
+ status : 200 ,
1638
+ json : async ( ) => validMetadata
1639
+ } ) ;
1640
+ }
1641
+
1642
+ return Promise . reject ( new Error ( `Unexpected fetch call: ${ urlString } ` ) ) ;
1643
+ } ) ;
1644
+
1645
+ const mockProvider : OAuthClientProvider = {
1646
+ redirectUrl : "http://localhost:3000/callback" ,
1647
+ clientMetadata : {
1648
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
1649
+ client_name : "Test Client"
1650
+ } ,
1651
+ clientInformation : ( ) => validClientInfo ,
1652
+ tokens : jest . fn ( ) ,
1653
+ saveTokens : jest . fn ( ) ,
1654
+ redirectToAuthorization : jest . fn ( ) ,
1655
+ saveCodeVerifier : jest . fn ( ) ,
1656
+ codeVerifier : ( ) => "test_verifier" ,
1657
+ delegateAuthorization : jest . fn ( ) . mockResolvedValue ( "AUTHORIZED" )
1658
+ } ;
1659
+
1660
+ const result = await auth ( mockProvider , { serverUrl : "https://api.example.com" } ) ;
1661
+
1662
+ expect ( result ) . toBe ( "AUTHORIZED" ) ;
1663
+ expect ( mockProvider . delegateAuthorization ) . toHaveBeenCalledWith (
1664
+ "https://auth.example.com" ,
1665
+ {
1666
+ resource : new URL ( "https://api.example.com/" ) ,
1667
+ metadata : expect . objectContaining ( validMetadata )
1668
+ }
1669
+ ) ;
1670
+ } ) ;
1671
+ } ) ;
1466
1672
} ) ;
1467
1673
} ) ;
0 commit comments