@@ -359,7 +359,7 @@ describe("OAuth Authorization", () => {
359
359
code_challenge_methods_supported : [ "S256" ] ,
360
360
} ;
361
361
362
- it ( "returns metadata when discovery succeeds" , async ( ) => {
362
+ it ( "returns metadata when oauth-authorization-server discovery succeeds" , async ( ) => {
363
363
mockFetch . mockResolvedValueOnce ( {
364
364
ok : true ,
365
365
status : 200 ,
@@ -377,6 +377,28 @@ describe("OAuth Authorization", () => {
377
377
} ) ;
378
378
} ) ;
379
379
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
+
380
402
it ( "returns metadata when discovery succeeds with path" , async ( ) => {
381
403
mockFetch . mockResolvedValueOnce ( {
382
404
ok : true ,
@@ -395,14 +417,14 @@ describe("OAuth Authorization", () => {
395
417
} ) ;
396
418
} ) ;
397
419
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
400
422
mockFetch . mockResolvedValueOnce ( {
401
423
ok : false ,
402
424
status : 404 ,
403
425
} ) ;
404
426
405
- // Second call (root fallback ) succeeds
427
+ // Second call (OIDC with path insertion ) succeeds
406
428
mockFetch . mockResolvedValueOnce ( {
407
429
ok : true ,
408
430
status : 200 ,
@@ -415,29 +437,35 @@ describe("OAuth Authorization", () => {
415
437
const calls = mockFetch . mock . calls ;
416
438
expect ( calls . length ) . toBe ( 2 ) ;
417
439
418
- // First call should be path-aware
440
+ // First call should be OAuth with path insertion
419
441
const [ firstUrl , firstOptions ] = calls [ 0 ] ;
420
442
expect ( firstUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server/path/name" ) ;
421
443
expect ( firstOptions . headers ) . toEqual ( {
422
444
"MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
423
445
} ) ;
424
446
425
- // Second call should be root fallback
447
+ // Second call should be OIDC with path insertion
426
448
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 " ) ;
428
450
expect ( secondOptions . headers ) . toEqual ( {
429
451
"MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
430
452
} ) ;
431
453
} ) ;
432
454
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
435
457
mockFetch . mockResolvedValueOnce ( {
436
458
ok : false ,
437
459
status : 404 ,
438
460
} ) ;
439
461
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
441
469
mockFetch . mockResolvedValueOnce ( {
442
470
ok : false ,
443
471
status : 404 ,
@@ -447,7 +475,33 @@ describe("OAuth Authorization", () => {
447
475
expect ( metadata ) . toBeUndefined ( ) ;
448
476
449
477
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" ) ;
451
505
} ) ;
452
506
453
507
it ( "does not fallback when the original URL is already at root path" , async ( ) => {
@@ -457,11 +511,17 @@ describe("OAuth Authorization", () => {
457
511
status : 404 ,
458
512
} ) ;
459
513
514
+ // Second call (OIDC discovery) also returns 404
515
+ mockFetch . mockResolvedValueOnce ( {
516
+ ok : false ,
517
+ status : 404 ,
518
+ } ) ;
519
+
460
520
const metadata = await discoverOAuthMetadata ( "https://auth.example.com/" ) ;
461
521
expect ( metadata ) . toBeUndefined ( ) ;
462
522
463
523
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
465
525
466
526
const [ url ] = calls [ 0 ] ;
467
527
expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
@@ -474,27 +534,42 @@ describe("OAuth Authorization", () => {
474
534
status : 404 ,
475
535
} ) ;
476
536
537
+ // Second call (OIDC discovery) also returns 404
538
+ mockFetch . mockResolvedValueOnce ( {
539
+ ok : false ,
540
+ status : 404 ,
541
+ } ) ;
542
+
477
543
const metadata = await discoverOAuthMetadata ( "https://auth.example.com" ) ;
478
544
expect ( metadata ) . toBeUndefined ( ) ;
479
545
480
546
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
482
548
483
549
const [ url ] = calls [ 0 ] ;
484
550
expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
485
551
} ) ;
486
552
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)
489
555
mockFetch . mockImplementationOnce ( ( ) => Promise . reject ( new TypeError ( "CORS error" ) ) ) ;
490
556
491
- // Retry path-aware without headers (simulating CORS retry)
557
+ // Retry OAuth with path insertion without headers (simulating CORS retry)
492
558
mockFetch . mockResolvedValueOnce ( {
493
559
ok : false ,
494
560
status : 404 ,
495
561
} ) ;
496
562
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
498
573
mockFetch . mockResolvedValueOnce ( {
499
574
ok : true ,
500
575
status : 200 ,
@@ -505,11 +580,11 @@ describe("OAuth Authorization", () => {
505
580
expect ( metadata ) . toEqual ( validMetadata ) ;
506
581
507
582
const calls = mockFetch . mock . calls ;
508
- expect ( calls . length ) . toBe ( 3 ) ;
583
+ expect ( calls . length ) . toBe ( 5 ) ;
509
584
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 " ) ;
513
588
expect ( lastOptions . headers ) . toEqual ( {
514
589
"MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
515
590
} ) ;
@@ -587,13 +662,14 @@ describe("OAuth Authorization", () => {
587
662
} ) ;
588
663
589
664
it ( "returns undefined when discovery endpoint returns 404" , async ( ) => {
590
- mockFetch . mockResolvedValueOnce ( {
665
+ mockFetch . mockResolvedValue ( {
591
666
ok : false ,
592
667
status : 404 ,
593
668
} ) ;
594
669
595
670
const metadata = await discoverOAuthMetadata ( "https://auth.example.com" ) ;
596
671
expect ( metadata ) . toBeUndefined ( ) ;
672
+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 2 ) ;
597
673
} ) ;
598
674
599
675
it ( "throws on non-404 errors" , async ( ) => {
0 commit comments