@@ -217,7 +217,7 @@ describe("OAuth Authorization", () => {
217
217
ok : false ,
218
218
status : 404 ,
219
219
} ) ;
220
-
220
+
221
221
// Second call (root fallback) succeeds
222
222
mockFetch . mockResolvedValueOnce ( {
223
223
ok : true ,
@@ -227,17 +227,17 @@ describe("OAuth Authorization", () => {
227
227
228
228
const metadata = await discoverOAuthProtectedResourceMetadata ( "https://resource.example.com/path/name" ) ;
229
229
expect ( metadata ) . toEqual ( validMetadata ) ;
230
-
230
+
231
231
const calls = mockFetch . mock . calls ;
232
232
expect ( calls . length ) . toBe ( 2 ) ;
233
-
233
+
234
234
// First call should be path-aware
235
235
const [ firstUrl , firstOptions ] = calls [ 0 ] ;
236
236
expect ( firstUrl . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource/path/name" ) ;
237
237
expect ( firstOptions . headers ) . toEqual ( {
238
238
"MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
239
239
} ) ;
240
-
240
+
241
241
// Second call should be root fallback
242
242
const [ secondUrl , secondOptions ] = calls [ 1 ] ;
243
243
expect ( secondUrl . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource" ) ;
@@ -252,7 +252,7 @@ describe("OAuth Authorization", () => {
252
252
ok : false ,
253
253
status : 404 ,
254
254
} ) ;
255
-
255
+
256
256
// Second call (root fallback) also returns 404
257
257
mockFetch . mockResolvedValueOnce ( {
258
258
ok : false ,
@@ -261,7 +261,7 @@ describe("OAuth Authorization", () => {
261
261
262
262
await expect ( discoverOAuthProtectedResourceMetadata ( "https://resource.example.com/path/name" ) )
263
263
. rejects . toThrow ( "Resource server does not implement OAuth 2.0 Protected Resource Metadata." ) ;
264
-
264
+
265
265
const calls = mockFetch . mock . calls ;
266
266
expect ( calls . length ) . toBe ( 2 ) ;
267
267
} ) ;
@@ -275,10 +275,10 @@ describe("OAuth Authorization", () => {
275
275
276
276
await expect ( discoverOAuthProtectedResourceMetadata ( "https://resource.example.com/" ) )
277
277
. rejects . toThrow ( "Resource server does not implement OAuth 2.0 Protected Resource Metadata." ) ;
278
-
278
+
279
279
const calls = mockFetch . mock . calls ;
280
280
expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback
281
-
281
+
282
282
const [ url ] = calls [ 0 ] ;
283
283
expect ( url . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource" ) ;
284
284
} ) ;
@@ -292,24 +292,24 @@ describe("OAuth Authorization", () => {
292
292
293
293
await expect ( discoverOAuthProtectedResourceMetadata ( "https://resource.example.com" ) )
294
294
. rejects . toThrow ( "Resource server does not implement OAuth 2.0 Protected Resource Metadata." ) ;
295
-
295
+
296
296
const calls = mockFetch . mock . calls ;
297
297
expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback
298
-
298
+
299
299
const [ url ] = calls [ 0 ] ;
300
300
expect ( url . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource" ) ;
301
301
} ) ;
302
302
303
303
it ( "falls back when path-aware discovery encounters CORS error" , async ( ) => {
304
304
// First call (path-aware) fails with TypeError (CORS)
305
305
mockFetch . mockImplementationOnce ( ( ) => Promise . reject ( new TypeError ( "CORS error" ) ) ) ;
306
-
306
+
307
307
// Retry path-aware without headers (simulating CORS retry)
308
308
mockFetch . mockResolvedValueOnce ( {
309
309
ok : false ,
310
310
status : 404 ,
311
311
} ) ;
312
-
312
+
313
313
// Second call (root fallback) succeeds
314
314
mockFetch . mockResolvedValueOnce ( {
315
315
ok : true ,
@@ -319,10 +319,10 @@ describe("OAuth Authorization", () => {
319
319
320
320
const metadata = await discoverOAuthProtectedResourceMetadata ( "https://resource.example.com/deep/path" ) ;
321
321
expect ( metadata ) . toEqual ( validMetadata ) ;
322
-
322
+
323
323
const calls = mockFetch . mock . calls ;
324
324
expect ( calls . length ) . toBe ( 3 ) ;
325
-
325
+
326
326
// Final call should be root fallback
327
327
const [ lastUrl , lastOptions ] = calls [ 2 ] ;
328
328
expect ( lastUrl . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource" ) ;
@@ -341,10 +341,10 @@ describe("OAuth Authorization", () => {
341
341
await expect ( discoverOAuthProtectedResourceMetadata ( "https://resource.example.com/path" , {
342
342
resourceMetadataUrl : "https://custom.example.com/metadata"
343
343
} ) ) . rejects . toThrow ( "Resource server does not implement OAuth 2.0 Protected Resource Metadata." ) ;
344
-
344
+
345
345
const calls = mockFetch . mock . calls ;
346
346
expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback when explicit URL is provided
347
-
347
+
348
348
const [ url ] = calls [ 0 ] ;
349
349
expect ( url . toString ( ) ) . toBe ( "https://custom.example.com/metadata" ) ;
350
350
} ) ;
@@ -749,10 +749,10 @@ describe("OAuth Authorization", () => {
749
749
expect ( metadata ) . toEqual ( validOpenIdMetadata ) ;
750
750
const calls = mockFetch . mock . calls ;
751
751
expect ( calls . length ) . toBe ( 2 ) ;
752
-
752
+
753
753
// First call should be OAuth discovery
754
754
expect ( calls [ 0 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
755
-
755
+
756
756
// Second call should be OpenID Connect discovery
757
757
expect ( calls [ 1 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/openid-configuration" ) ;
758
758
} ) ;
@@ -815,10 +815,10 @@ describe("OAuth Authorization", () => {
815
815
expect ( metadata ) . toEqual ( validOpenIdMetadata ) ;
816
816
const calls = mockFetch . mock . calls ;
817
817
expect ( calls . length ) . toBe ( 2 ) ;
818
-
818
+
819
819
// First call should be OAuth with path
820
820
expect ( calls [ 0 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server/tenant1" ) ;
821
-
821
+
822
822
// Second call should be OpenID Connect with path insertion
823
823
expect ( calls [ 1 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/openid-configuration/tenant1" ) ;
824
824
} ) ;
@@ -851,17 +851,44 @@ describe("OAuth Authorization", () => {
851
851
expect ( metadata ) . toEqual ( validOpenIdMetadata ) ;
852
852
const calls = mockFetch . mock . calls ;
853
853
expect ( calls . length ) . toBe ( 3 ) ;
854
-
854
+
855
855
// First call should be OAuth with path
856
856
expect ( calls [ 0 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server/tenant1" ) ;
857
-
857
+
858
858
// Second call should be OpenID Connect with path insertion
859
859
expect ( calls [ 1 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/openid-configuration/tenant1" ) ;
860
-
860
+
861
861
// Third call should be OpenID Connect with path prepending
862
862
expect ( calls [ 2 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/tenant1/.well-known/openid-configuration" ) ;
863
863
} ) ;
864
864
865
+ it ( "throws error when OIDC provider does not support S256 PKCE" , async ( ) => {
866
+ // OAuth discovery fails
867
+ mockFetch . mockResolvedValueOnce ( {
868
+ ok : false ,
869
+ status : 404 ,
870
+ } ) ;
871
+
872
+ // OpenID Connect discovery succeeds but without S256 support
873
+ const invalidOpenIdMetadata = {
874
+ ...validOpenIdMetadata ,
875
+ code_challenge_methods_supported : [ "plain" ] , // Missing S256
876
+ } ;
877
+
878
+ mockFetch . mockResolvedValueOnce ( {
879
+ ok : true ,
880
+ status : 200 ,
881
+ json : async ( ) => invalidOpenIdMetadata ,
882
+ } ) ;
883
+
884
+ await expect (
885
+ discoverAuthorizationServerMetadata (
886
+ "https://mcp.example.com" ,
887
+ "https://auth.example.com"
888
+ )
889
+ ) . rejects . toThrow ( "does not support S256 code challenge method required by MCP specification" ) ;
890
+ } ) ;
891
+
865
892
it ( "falls back to legacy MCP server when authorizationServerUrl is undefined" , async ( ) => {
866
893
mockFetch . mockResolvedValueOnce ( {
867
894
ok : true ,
@@ -916,7 +943,7 @@ describe("OAuth Authorization", () => {
916
943
it ( "handles CORS errors with retry" , async ( ) => {
917
944
// First call fails with CORS
918
945
mockFetch . mockImplementationOnce ( ( ) => Promise . reject ( new TypeError ( "CORS error" ) ) ) ;
919
-
946
+
920
947
// Retry without headers succeeds
921
948
mockFetch . mockResolvedValueOnce ( {
922
949
ok : true ,
@@ -932,10 +959,10 @@ describe("OAuth Authorization", () => {
932
959
expect ( metadata ) . toEqual ( validOAuthMetadata ) ;
933
960
const calls = mockFetch . mock . calls ;
934
961
expect ( calls . length ) . toBe ( 2 ) ;
935
-
962
+
936
963
// First call should have headers
937
964
expect ( calls [ 0 ] [ 1 ] ?. headers ) . toHaveProperty ( "MCP-Protocol-Version" ) ;
938
-
965
+
939
966
// Second call should not have headers (CORS retry)
940
967
expect ( calls [ 1 ] [ 1 ] ?. headers ) . toBeUndefined ( ) ;
941
968
} ) ;
@@ -2216,17 +2243,17 @@ describe("OAuth Authorization", () => {
2216
2243
2217
2244
// Verify the correct URLs were fetched
2218
2245
const calls = mockFetch . mock . calls ;
2219
-
2246
+
2220
2247
// First call should be to PRM
2221
2248
expect ( calls [ 0 ] [ 0 ] . toString ( ) ) . toBe ( "https://my.resource.com/.well-known/oauth-protected-resource/path/name" ) ;
2222
-
2249
+
2223
2250
// Second call should be to AS metadata with the path from authorization server
2224
2251
expect ( calls [ 1 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server/oauth" ) ;
2225
2252
} ) ;
2226
2253
2227
2254
it ( "supports overriding the fetch function used for requests" , async ( ) => {
2228
2255
const customFetch = jest . fn ( ) ;
2229
-
2256
+
2230
2257
// Mock PRM discovery
2231
2258
customFetch . mockResolvedValueOnce ( {
2232
2259
ok : true ,
@@ -2236,7 +2263,7 @@ describe("OAuth Authorization", () => {
2236
2263
authorization_servers : [ "https://auth.example.com" ] ,
2237
2264
} ) ,
2238
2265
} ) ;
2239
-
2266
+
2240
2267
// Mock AS metadata discovery
2241
2268
customFetch . mockResolvedValueOnce ( {
2242
2269
ok : true ,
@@ -2253,7 +2280,7 @@ describe("OAuth Authorization", () => {
2253
2280
2254
2281
const mockProvider : OAuthClientProvider = {
2255
2282
get redirectUrl ( ) { return "http://localhost:3000/callback" ; } ,
2256
- get clientMetadata ( ) {
2283
+ get clientMetadata ( ) {
2257
2284
return {
2258
2285
client_name : "Test Client" ,
2259
2286
redirect_uris : [ "http://localhost:3000/callback" ] ,
@@ -2278,10 +2305,10 @@ describe("OAuth Authorization", () => {
2278
2305
expect ( result ) . toBe ( "REDIRECT" ) ;
2279
2306
expect ( customFetch ) . toHaveBeenCalledTimes ( 2 ) ;
2280
2307
expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
2281
-
2308
+
2282
2309
// Verify custom fetch was called for PRM discovery
2283
2310
expect ( customFetch . mock . calls [ 0 ] [ 0 ] . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource" ) ;
2284
-
2311
+
2285
2312
// Verify custom fetch was called for AS metadata discovery
2286
2313
expect ( customFetch . mock . calls [ 1 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
2287
2314
} ) ;
0 commit comments