@@ -231,7 +231,7 @@ describe("OAuth Authorization", () => {
231231 ok : false ,
232232 status : 404 ,
233233 } ) ;
234-
234+
235235 // Second call (root fallback) succeeds
236236 mockFetch . mockResolvedValueOnce ( {
237237 ok : true ,
@@ -241,17 +241,17 @@ describe("OAuth Authorization", () => {
241241
242242 const metadata = await discoverOAuthMetadata ( "https://auth.example.com/path/name" ) ;
243243 expect ( metadata ) . toEqual ( validMetadata ) ;
244-
244+
245245 const calls = mockFetch . mock . calls ;
246246 expect ( calls . length ) . toBe ( 2 ) ;
247-
247+
248248 // First call should be path-aware
249249 const [ firstUrl , firstOptions ] = calls [ 0 ] ;
250250 expect ( firstUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server/path/name" ) ;
251251 expect ( firstOptions . headers ) . toEqual ( {
252252 "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
253253 } ) ;
254-
254+
255255 // Second call should be root fallback
256256 const [ secondUrl , secondOptions ] = calls [ 1 ] ;
257257 expect ( secondUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
@@ -266,7 +266,7 @@ describe("OAuth Authorization", () => {
266266 ok : false ,
267267 status : 404 ,
268268 } ) ;
269-
269+
270270 // Second call (root fallback) also returns 404
271271 mockFetch . mockResolvedValueOnce ( {
272272 ok : false ,
@@ -275,7 +275,7 @@ describe("OAuth Authorization", () => {
275275
276276 const metadata = await discoverOAuthMetadata ( "https://auth.example.com/path/name" ) ;
277277 expect ( metadata ) . toBeUndefined ( ) ;
278-
278+
279279 const calls = mockFetch . mock . calls ;
280280 expect ( calls . length ) . toBe ( 2 ) ;
281281 } ) ;
@@ -289,10 +289,10 @@ describe("OAuth Authorization", () => {
289289
290290 const metadata = await discoverOAuthMetadata ( "https://auth.example.com/" ) ;
291291 expect ( metadata ) . toBeUndefined ( ) ;
292-
292+
293293 const calls = mockFetch . mock . calls ;
294294 expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback
295-
295+
296296 const [ url ] = calls [ 0 ] ;
297297 expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
298298 } ) ;
@@ -306,24 +306,24 @@ describe("OAuth Authorization", () => {
306306
307307 const metadata = await discoverOAuthMetadata ( "https://auth.example.com" ) ;
308308 expect ( metadata ) . toBeUndefined ( ) ;
309-
309+
310310 const calls = mockFetch . mock . calls ;
311311 expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback
312-
312+
313313 const [ url ] = calls [ 0 ] ;
314314 expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
315315 } ) ;
316316
317317 it ( "falls back when path-aware discovery encounters CORS error" , async ( ) => {
318318 // First call (path-aware) fails with TypeError (CORS)
319319 mockFetch . mockImplementationOnce ( ( ) => Promise . reject ( new TypeError ( "CORS error" ) ) ) ;
320-
320+
321321 // Retry path-aware without headers (simulating CORS retry)
322322 mockFetch . mockResolvedValueOnce ( {
323323 ok : false ,
324324 status : 404 ,
325325 } ) ;
326-
326+
327327 // Second call (root fallback) succeeds
328328 mockFetch . mockResolvedValueOnce ( {
329329 ok : true ,
@@ -333,10 +333,10 @@ describe("OAuth Authorization", () => {
333333
334334 const metadata = await discoverOAuthMetadata ( "https://auth.example.com/deep/path" ) ;
335335 expect ( metadata ) . toEqual ( validMetadata ) ;
336-
336+
337337 const calls = mockFetch . mock . calls ;
338338 expect ( calls . length ) . toBe ( 3 ) ;
339-
339+
340340 // Final call should be root fallback
341341 const [ lastUrl , lastOptions ] = calls [ 2 ] ;
342342 expect ( lastUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
@@ -1463,5 +1463,211 @@ describe("OAuth Authorization", () => {
14631463 expect ( body . get ( "grant_type" ) ) . toBe ( "refresh_token" ) ;
14641464 expect ( body . get ( "refresh_token" ) ) . toBe ( "refresh123" ) ;
14651465 } ) ;
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+ } ) ;
14661672 } ) ;
14671673} ) ;
0 commit comments