@@ -14,6 +14,7 @@ import {
1414} from "./auth.js" ;
1515import { ServerError } from "../server/auth/errors.js" ;
1616import { AuthorizationServerMetadata } from '../shared/auth.js' ;
17+ import { OAuthClientMetadata , OAuthProtectedResourceMetadata } from '../shared/auth.js' ;
1718
1819// Mock fetch globally
1920const mockFetch = jest . fn ( ) ;
@@ -1457,6 +1458,48 @@ describe("OAuth Authorization", () => {
14571458 ) ;
14581459 } ) ;
14591460
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+
14601503 it ( "validates client information response schema" , async ( ) => {
14611504 mockFetch . mockResolvedValueOnce ( {
14621505 ok : true ,
@@ -1799,6 +1842,64 @@ describe("OAuth Authorization", () => {
17991842 expect ( body . get ( "refresh_token" ) ) . toBe ( "refresh123" ) ;
18001843 } ) ;
18011844
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+
18021903 it ( "skips default PRM resource validation when custom validateResourceURL is provided" , async ( ) => {
18031904 const mockValidateResourceURL = jest . fn ( ) . mockResolvedValue ( undefined ) ;
18041905 const providerWithCustomValidation = {
0 commit comments