@@ -14,6 +14,7 @@ import {
14
14
} from "./auth.js" ;
15
15
import { ServerError } from "../server/auth/errors.js" ;
16
16
import { AuthorizationServerMetadata } from '../shared/auth.js' ;
17
+ import { OAuthClientMetadata , OAuthProtectedResourceMetadata } from '../shared/auth.js' ;
17
18
18
19
// Mock fetch globally
19
20
const mockFetch = jest . fn ( ) ;
@@ -1457,6 +1458,48 @@ describe("OAuth Authorization", () => {
1457
1458
) ;
1458
1459
} ) ;
1459
1460
1461
+ it ( "registers client with scopes_supported from resourceMetadata if scope is not provided" , async ( ) => {
1462
+ const resourceMetadata : OAuthProtectedResourceMetadata = {
1463
+ scopes_supported : [ "openid" , "profile" ] ,
1464
+ resource : "https://api.example.com/mcp-server" ,
1465
+ } ;
1466
+
1467
+ const validClientMetadataWithoutScope : OAuthClientMetadata = {
1468
+ ...validClientMetadata ,
1469
+ scope : undefined ,
1470
+ } ;
1471
+
1472
+ const expectedClientInfo = {
1473
+ ...validClientInfo ,
1474
+ scope : "openid profile" ,
1475
+ } ;
1476
+
1477
+ mockFetch . mockResolvedValueOnce ( {
1478
+ ok : true ,
1479
+ status : 200 ,
1480
+ json : async ( ) => expectedClientInfo ,
1481
+ } ) ;
1482
+
1483
+ const clientInfo = await registerClient ( "https://auth.example.com" , {
1484
+ clientMetadata : validClientMetadataWithoutScope ,
1485
+ resourceMetadata,
1486
+ } ) ;
1487
+
1488
+ expect ( clientInfo ) . toEqual ( expectedClientInfo ) ;
1489
+ expect ( mockFetch ) . toHaveBeenCalledWith (
1490
+ expect . objectContaining ( {
1491
+ href : "https://auth.example.com/register" ,
1492
+ } ) ,
1493
+ expect . objectContaining ( {
1494
+ method : "POST" ,
1495
+ headers : {
1496
+ "Content-Type" : "application/json" ,
1497
+ } ,
1498
+ body : JSON . stringify ( { ...validClientMetadata , scope : "openid profile" } ) ,
1499
+ } )
1500
+ ) ;
1501
+ } ) ;
1502
+
1460
1503
it ( "validates client information response schema" , async ( ) => {
1461
1504
mockFetch . mockResolvedValueOnce ( {
1462
1505
ok : true ,
@@ -1799,6 +1842,64 @@ describe("OAuth Authorization", () => {
1799
1842
expect ( body . get ( "refresh_token" ) ) . toBe ( "refresh123" ) ;
1800
1843
} ) ;
1801
1844
1845
+ it ( "uses scopes_supported from resource metadata if scope is not provided" , async ( ) => {
1846
+ // Mock successful metadata discovery - need to include protected resource metadata
1847
+ mockFetch . mockImplementation ( ( url ) => {
1848
+ const urlString = url . toString ( ) ;
1849
+ if ( urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
1850
+ return Promise . resolve ( {
1851
+ ok : true ,
1852
+ status : 200 ,
1853
+ json : async ( ) => ( {
1854
+ resource : "https://api.example.com/mcp-server" ,
1855
+ authorization_servers : [ "https://auth.example.com" ] ,
1856
+ scopes_supported : [ "openid" , "profile" ] ,
1857
+ } ) ,
1858
+ } ) ;
1859
+ } else if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
1860
+ return Promise . resolve ( {
1861
+ ok : true ,
1862
+ status : 200 ,
1863
+ json : async ( ) => ( {
1864
+ issuer : "https://auth.example.com" ,
1865
+ authorization_endpoint : "https://auth.example.com/authorize" ,
1866
+ token_endpoint : "https://auth.example.com/token" ,
1867
+ response_types_supported : [ "code" ] ,
1868
+ code_challenge_methods_supported : [ "S256" ] ,
1869
+ } ) ,
1870
+ } ) ;
1871
+ }
1872
+ return Promise . resolve ( { ok : false , status : 404 } ) ;
1873
+ } ) ;
1874
+
1875
+ // Mock provider methods for authorization flow
1876
+ ( mockProvider . clientInformation as jest . Mock ) . mockResolvedValue ( {
1877
+ client_id : "test-client" ,
1878
+ client_secret : "test-secret" ,
1879
+ } ) ;
1880
+ ( mockProvider . tokens as jest . Mock ) . mockResolvedValue ( undefined ) ;
1881
+ ( mockProvider . saveCodeVerifier as jest . Mock ) . mockResolvedValue ( undefined ) ;
1882
+ ( mockProvider . redirectToAuthorization as jest . Mock ) . mockResolvedValue ( undefined ) ;
1883
+
1884
+ // Call auth without authorization code (should trigger redirect)
1885
+ const result = await auth ( mockProvider , {
1886
+ serverUrl : "https://api.example.com/mcp-server" ,
1887
+ } ) ;
1888
+
1889
+ expect ( result ) . toBe ( "REDIRECT" ) ;
1890
+
1891
+ // Verify the authorization URL includes the resource parameter
1892
+ expect ( mockProvider . redirectToAuthorization ) . toHaveBeenCalledWith (
1893
+ expect . objectContaining ( {
1894
+ searchParams : expect . any ( URLSearchParams ) ,
1895
+ } )
1896
+ ) ;
1897
+
1898
+ const redirectCall = ( mockProvider . redirectToAuthorization as jest . Mock ) . mock . calls [ 0 ] ;
1899
+ const authUrl : URL = redirectCall [ 0 ] ;
1900
+ expect ( authUrl . searchParams . get ( "scope" ) ) . toBe ( "openid profile" ) ;
1901
+ } ) ;
1902
+
1802
1903
it ( "skips default PRM resource validation when custom validateResourceURL is provided" , async ( ) => {
1803
1904
const mockValidateResourceURL = jest . fn ( ) . mockResolvedValue ( undefined ) ;
1804
1905
const providerWithCustomValidation = {
0 commit comments