@@ -359,7 +359,7 @@ describe("OAuth Authorization", () => {
359359 code_challenge_methods_supported : [ "S256" ] ,
360360 } ;
361361
362- it ( "returns metadata when discovery succeeds" , async ( ) => {
362+ it ( "returns metadata when oauth-authorization-server discovery succeeds" , async ( ) => {
363363 mockFetch . mockResolvedValueOnce ( {
364364 ok : true ,
365365 status : 200 ,
@@ -377,6 +377,28 @@ describe("OAuth Authorization", () => {
377377 } ) ;
378378 } ) ;
379379
380+ it ( "returns metadata when oidc discovery succeeds" , async ( ) => {
381+ mockFetch . mockImplementation ( ( url ) => {
382+ if ( url . toString ( ) . includes ( 'openid-configuration' ) ) {
383+ return Promise . resolve ( {
384+ ok : true ,
385+ status : 200 ,
386+ json : async ( ) => validMetadata ,
387+ } ) ;
388+ }
389+ return Promise . resolve ( {
390+ ok : false ,
391+ status : 404 ,
392+ } ) ;
393+ } ) ;
394+
395+ const metadata = await discoverOAuthMetadata ( "https://auth.example.com" ) ;
396+ expect ( metadata ) . toEqual ( validMetadata ) ;
397+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 2 ) ;
398+ expect ( mockFetch . mock . calls [ 0 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
399+ expect ( mockFetch . mock . calls [ 1 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/openid-configuration" ) ;
400+ } ) ;
401+
380402 it ( "returns metadata when discovery succeeds with path" , async ( ) => {
381403 mockFetch . mockResolvedValueOnce ( {
382404 ok : true ,
@@ -395,14 +417,14 @@ describe("OAuth Authorization", () => {
395417 } ) ;
396418 } ) ;
397419
398- it ( "falls back to root discovery when path-aware discovery returns 404 " , async ( ) => {
399- // First call (path-aware ) returns 404
420+ it ( "tries discovery endpoints in new spec order for URLs with path " , async ( ) => {
421+ // First call (OAuth with path insertion ) returns 404
400422 mockFetch . mockResolvedValueOnce ( {
401423 ok : false ,
402424 status : 404 ,
403425 } ) ;
404426
405- // Second call (root fallback ) succeeds
427+ // Second call (OIDC with path insertion ) succeeds
406428 mockFetch . mockResolvedValueOnce ( {
407429 ok : true ,
408430 status : 200 ,
@@ -415,29 +437,35 @@ describe("OAuth Authorization", () => {
415437 const calls = mockFetch . mock . calls ;
416438 expect ( calls . length ) . toBe ( 2 ) ;
417439
418- // First call should be path-aware
440+ // First call should be OAuth with path insertion
419441 const [ firstUrl , firstOptions ] = calls [ 0 ] ;
420442 expect ( firstUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server/path/name" ) ;
421443 expect ( firstOptions . headers ) . toEqual ( {
422444 "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
423445 } ) ;
424446
425- // Second call should be root fallback
447+ // Second call should be OIDC with path insertion
426448 const [ secondUrl , secondOptions ] = calls [ 1 ] ;
427- expect ( secondUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server " ) ;
449+ expect ( secondUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/openid-configuration/path/name " ) ;
428450 expect ( secondOptions . headers ) . toEqual ( {
429451 "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
430452 } ) ;
431453 } ) ;
432454
433- it ( "returns undefined when both path-aware and root discovery return 404" , async ( ) => {
434- // First call (path-aware ) returns 404
455+ it ( "returns undefined when all discovery endpoints return 404" , async ( ) => {
456+ // First call (OAuth with path insertion ) returns 404
435457 mockFetch . mockResolvedValueOnce ( {
436458 ok : false ,
437459 status : 404 ,
438460 } ) ;
439461
440- // Second call (root fallback) also returns 404
462+ // Second call (OIDC with path insertion) returns 404
463+ mockFetch . mockResolvedValueOnce ( {
464+ ok : false ,
465+ status : 404 ,
466+ } ) ;
467+
468+ // Third call (OIDC with path appending) returns 404
441469 mockFetch . mockResolvedValueOnce ( {
442470 ok : false ,
443471 status : 404 ,
@@ -447,7 +475,33 @@ describe("OAuth Authorization", () => {
447475 expect ( metadata ) . toBeUndefined ( ) ;
448476
449477 const calls = mockFetch . mock . calls ;
450- expect ( calls . length ) . toBe ( 2 ) ;
478+ expect ( calls . length ) . toBe ( 3 ) ;
479+ } ) ;
480+
481+ it ( "tries all endpoints in correct order for URLs with path" , async ( ) => {
482+ // All calls return 404 to test the order
483+ mockFetch . mockResolvedValue ( {
484+ ok : false ,
485+ status : 404 ,
486+ } ) ;
487+
488+ const metadata = await discoverOAuthMetadata ( "https://auth.example.com/tenant1" ) ;
489+ expect ( metadata ) . toBeUndefined ( ) ;
490+
491+ const calls = mockFetch . mock . calls ;
492+ expect ( calls . length ) . toBe ( 3 ) ;
493+
494+ // First call should be OAuth 2.0 Authorization Server Metadata with path insertion
495+ const [ firstUrl ] = calls [ 0 ] ;
496+ expect ( firstUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server/tenant1" ) ;
497+
498+ // Second call should be OpenID Connect Discovery 1.0 with path insertion
499+ const [ secondUrl ] = calls [ 1 ] ;
500+ expect ( secondUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/openid-configuration/tenant1" ) ;
501+
502+ // Third call should be OpenID Connect Discovery 1.0 path appending
503+ const [ thirdUrl ] = calls [ 2 ] ;
504+ expect ( thirdUrl . toString ( ) ) . toBe ( "https://auth.example.com/tenant1/.well-known/openid-configuration" ) ;
451505 } ) ;
452506
453507 it ( "does not fallback when the original URL is already at root path" , async ( ) => {
@@ -457,11 +511,17 @@ describe("OAuth Authorization", () => {
457511 status : 404 ,
458512 } ) ;
459513
514+ // Second call (OIDC discovery) also returns 404
515+ mockFetch . mockResolvedValueOnce ( {
516+ ok : false ,
517+ status : 404 ,
518+ } ) ;
519+
460520 const metadata = await discoverOAuthMetadata ( "https://auth.example.com/" ) ;
461521 expect ( metadata ) . toBeUndefined ( ) ;
462522
463523 const calls = mockFetch . mock . calls ;
464- expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback
524+ expect ( calls . length ) . toBe ( 2 ) ; // Should not attempt fallback but will try OIDC
465525
466526 const [ url ] = calls [ 0 ] ;
467527 expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
@@ -474,27 +534,42 @@ describe("OAuth Authorization", () => {
474534 status : 404 ,
475535 } ) ;
476536
537+ // Second call (OIDC discovery) also returns 404
538+ mockFetch . mockResolvedValueOnce ( {
539+ ok : false ,
540+ status : 404 ,
541+ } ) ;
542+
477543 const metadata = await discoverOAuthMetadata ( "https://auth.example.com" ) ;
478544 expect ( metadata ) . toBeUndefined ( ) ;
479545
480546 const calls = mockFetch . mock . calls ;
481- expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback
547+ expect ( calls . length ) . toBe ( 2 ) ; // Should not attempt fallback but will try OIDC
482548
483549 const [ url ] = calls [ 0 ] ;
484550 expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
485551 } ) ;
486552
487- it ( "falls back when path-aware discovery encounters CORS error" , async ( ) => {
488- // First call (path-aware ) fails with TypeError (CORS)
553+ it ( "tries all endpoints when discovery encounters CORS error" , async ( ) => {
554+ // First call (OAuth with path insertion ) fails with TypeError (CORS)
489555 mockFetch . mockImplementationOnce ( ( ) => Promise . reject ( new TypeError ( "CORS error" ) ) ) ;
490556
491- // Retry path-aware without headers (simulating CORS retry)
557+ // Retry OAuth with path insertion without headers (simulating CORS retry)
492558 mockFetch . mockResolvedValueOnce ( {
493559 ok : false ,
494560 status : 404 ,
495561 } ) ;
496562
497- // Second call (root fallback) succeeds
563+ // Second call (OIDC with path insertion) fails with TypeError (CORS)
564+ mockFetch . mockImplementationOnce ( ( ) => Promise . reject ( new TypeError ( "CORS error" ) ) ) ;
565+
566+ // Retry OIDC with path insertion without headers (simulating CORS retry)
567+ mockFetch . mockResolvedValueOnce ( {
568+ ok : false ,
569+ status : 404 ,
570+ } ) ;
571+
572+ // Third call (OIDC with path appending) succeeds
498573 mockFetch . mockResolvedValueOnce ( {
499574 ok : true ,
500575 status : 200 ,
@@ -505,11 +580,11 @@ describe("OAuth Authorization", () => {
505580 expect ( metadata ) . toEqual ( validMetadata ) ;
506581
507582 const calls = mockFetch . mock . calls ;
508- expect ( calls . length ) . toBe ( 3 ) ;
583+ expect ( calls . length ) . toBe ( 5 ) ;
509584
510- // Final call should be root fallback
511- const [ lastUrl , lastOptions ] = calls [ 2 ] ;
512- expect ( lastUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server " ) ;
585+ // Final call should be OIDC with path appending
586+ const [ lastUrl , lastOptions ] = calls [ 4 ] ;
587+ expect ( lastUrl . toString ( ) ) . toBe ( "https://auth.example.com/deep/path/ .well-known/openid-configuration " ) ;
513588 expect ( lastOptions . headers ) . toEqual ( {
514589 "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
515590 } ) ;
@@ -587,13 +662,14 @@ describe("OAuth Authorization", () => {
587662 } ) ;
588663
589664 it ( "returns undefined when discovery endpoint returns 404" , async ( ) => {
590- mockFetch . mockResolvedValueOnce ( {
665+ mockFetch . mockResolvedValue ( {
591666 ok : false ,
592667 status : 404 ,
593668 } ) ;
594669
595670 const metadata = await discoverOAuthMetadata ( "https://auth.example.com" ) ;
596671 expect ( metadata ) . toBeUndefined ( ) ;
672+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 2 ) ;
597673 } ) ;
598674
599675 it ( "throws on non-404 errors" , async ( ) => {
0 commit comments