@@ -10,7 +10,7 @@ import {
10
10
auth ,
11
11
type OAuthClientProvider ,
12
12
} from "./auth.js" ;
13
- import { OAuthMetadata } from '../shared/auth.js' ;
13
+ import { OAuthClientMetadata , OAuthMetadata , OAuthProtectedResourceMetadata } from '../shared/auth.js' ;
14
14
15
15
// Mock fetch globally
16
16
const mockFetch = jest . fn ( ) ;
@@ -926,6 +926,48 @@ describe("OAuth Authorization", () => {
926
926
) ;
927
927
} ) ;
928
928
929
+ it ( "registers client with scopes_supported from resourceMetadata if scope is not provided" , async ( ) => {
930
+ const resourceMetadata : OAuthProtectedResourceMetadata = {
931
+ scopes_supported : [ "openid" , "profile" ] ,
932
+ resource : "https://api.example.com/mcp-server" ,
933
+ } ;
934
+
935
+ const validClientMetadataWithoutScope : OAuthClientMetadata = {
936
+ ...validClientMetadata ,
937
+ scope : undefined ,
938
+ } ;
939
+
940
+ const expectedClientInfo = {
941
+ ...validClientInfo ,
942
+ scope : "openid profile" ,
943
+ } ;
944
+
945
+ mockFetch . mockResolvedValueOnce ( {
946
+ ok : true ,
947
+ status : 200 ,
948
+ json : async ( ) => expectedClientInfo ,
949
+ } ) ;
950
+
951
+ const clientInfo = await registerClient ( "https://auth.example.com" , {
952
+ clientMetadata : validClientMetadataWithoutScope ,
953
+ resourceMetadata,
954
+ } ) ;
955
+
956
+ expect ( clientInfo ) . toEqual ( expectedClientInfo ) ;
957
+ expect ( mockFetch ) . toHaveBeenCalledWith (
958
+ expect . objectContaining ( {
959
+ href : "https://auth.example.com/register" ,
960
+ } ) ,
961
+ expect . objectContaining ( {
962
+ method : "POST" ,
963
+ headers : {
964
+ "Content-Type" : "application/json" ,
965
+ } ,
966
+ body : JSON . stringify ( { ...validClientMetadata , scope : "openid profile" } ) ,
967
+ } )
968
+ ) ;
969
+ } ) ;
970
+
929
971
it ( "validates client information response schema" , async ( ) => {
930
972
mockFetch . mockResolvedValueOnce ( {
931
973
ok : true ,
@@ -1266,6 +1308,64 @@ describe("OAuth Authorization", () => {
1266
1308
expect ( body . get ( "refresh_token" ) ) . toBe ( "refresh123" ) ;
1267
1309
} ) ;
1268
1310
1311
+ it ( "uses scopes_supported from resource metadata if scope is not provided" , async ( ) => {
1312
+ // Mock successful metadata discovery - need to include protected resource metadata
1313
+ mockFetch . mockImplementation ( ( url ) => {
1314
+ const urlString = url . toString ( ) ;
1315
+ if ( urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
1316
+ return Promise . resolve ( {
1317
+ ok : true ,
1318
+ status : 200 ,
1319
+ json : async ( ) => ( {
1320
+ resource : "https://api.example.com/mcp-server" ,
1321
+ authorization_servers : [ "https://auth.example.com" ] ,
1322
+ scopes_supported : [ "openid" , "profile" ] ,
1323
+ } ) ,
1324
+ } ) ;
1325
+ } else if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
1326
+ return Promise . resolve ( {
1327
+ ok : true ,
1328
+ status : 200 ,
1329
+ json : async ( ) => ( {
1330
+ issuer : "https://auth.example.com" ,
1331
+ authorization_endpoint : "https://auth.example.com/authorize" ,
1332
+ token_endpoint : "https://auth.example.com/token" ,
1333
+ response_types_supported : [ "code" ] ,
1334
+ code_challenge_methods_supported : [ "S256" ] ,
1335
+ } ) ,
1336
+ } ) ;
1337
+ }
1338
+ return Promise . resolve ( { ok : false , status : 404 } ) ;
1339
+ } ) ;
1340
+
1341
+ // Mock provider methods for authorization flow
1342
+ ( mockProvider . clientInformation as jest . Mock ) . mockResolvedValue ( {
1343
+ client_id : "test-client" ,
1344
+ client_secret : "test-secret" ,
1345
+ } ) ;
1346
+ ( mockProvider . tokens as jest . Mock ) . mockResolvedValue ( undefined ) ;
1347
+ ( mockProvider . saveCodeVerifier as jest . Mock ) . mockResolvedValue ( undefined ) ;
1348
+ ( mockProvider . redirectToAuthorization as jest . Mock ) . mockResolvedValue ( undefined ) ;
1349
+
1350
+ // Call auth without authorization code (should trigger redirect)
1351
+ const result = await auth ( mockProvider , {
1352
+ serverUrl : "https://api.example.com/mcp-server" ,
1353
+ } ) ;
1354
+
1355
+ expect ( result ) . toBe ( "REDIRECT" ) ;
1356
+
1357
+ // Verify the authorization URL includes the resource parameter
1358
+ expect ( mockProvider . redirectToAuthorization ) . toHaveBeenCalledWith (
1359
+ expect . objectContaining ( {
1360
+ searchParams : expect . any ( URLSearchParams ) ,
1361
+ } )
1362
+ ) ;
1363
+
1364
+ const redirectCall = ( mockProvider . redirectToAuthorization as jest . Mock ) . mock . calls [ 0 ] ;
1365
+ const authUrl : URL = redirectCall [ 0 ] ;
1366
+ expect ( authUrl . searchParams . get ( "scope" ) ) . toBe ( "openid profile" ) ;
1367
+ } ) ;
1368
+
1269
1369
it ( "skips default PRM resource validation when custom validateResourceURL is provided" , async ( ) => {
1270
1370
const mockValidateResourceURL = jest . fn ( ) . mockResolvedValue ( undefined ) ;
1271
1371
const providerWithCustomValidation = {
0 commit comments