@@ -13,6 +13,7 @@ import {
1313 discoverOAuthServerInfo ,
1414 exchangeAuthorization ,
1515 extractWWWAuthenticateParams ,
16+ handleOAuthUnauthorized ,
1617 isHttpsUrl ,
1718 refreshAuthorization ,
1819 registerClient ,
@@ -2059,6 +2060,66 @@ describe('OAuth Authorization', () => {
20592060 vi . clearAllMocks ( ) ;
20602061 } ) ;
20612062
2063+ it ( 'prefers accumulated scope when handling interactive 401 re-authorization' , async ( ) => {
2064+ mockFetch . mockImplementation ( url => {
2065+ const urlString = url . toString ( ) ;
2066+
2067+ if ( urlString === 'https://api.example.com/.well-known/oauth-protected-resource' ) {
2068+ return Promise . resolve ( {
2069+ ok : true ,
2070+ status : 200 ,
2071+ json : async ( ) => ( {
2072+ resource : 'https://api.example.com/mcp-server' ,
2073+ authorization_servers : [ 'https://auth.example.com' ]
2074+ } )
2075+ } ) ;
2076+ }
2077+
2078+ if ( urlString === 'https://auth.example.com/.well-known/oauth-authorization-server' ) {
2079+ return Promise . resolve ( {
2080+ ok : true ,
2081+ status : 200 ,
2082+ json : async ( ) => ( {
2083+ issuer : 'https://auth.example.com' ,
2084+ authorization_endpoint : 'https://auth.example.com/authorize' ,
2085+ token_endpoint : 'https://auth.example.com/token' ,
2086+ response_types_supported : [ 'code' ] ,
2087+ code_challenge_methods_supported : [ 'S256' ]
2088+ } )
2089+ } ) ;
2090+ }
2091+
2092+ return Promise . reject ( new Error ( `Unexpected fetch call: ${ urlString } ` ) ) ;
2093+ } ) ;
2094+
2095+ vi . mocked ( mockProvider . clientInformation ) . mockResolvedValue ( {
2096+ client_id : 'test-client' ,
2097+ client_secret : 'test-secret'
2098+ } ) ;
2099+ vi . mocked ( mockProvider . tokens ) . mockResolvedValue ( undefined ) ;
2100+ vi . mocked ( mockProvider . saveCodeVerifier ) . mockResolvedValue ( undefined ) ;
2101+ vi . mocked ( mockProvider . redirectToAuthorization ) . mockResolvedValue ( undefined ) ;
2102+
2103+ await expect (
2104+ handleOAuthUnauthorized ( mockProvider , {
2105+ response : new Response ( null , {
2106+ status : 401 ,
2107+ headers : {
2108+ 'WWW-Authenticate' :
2109+ 'Bearer scope="read:op2", resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"'
2110+ }
2111+ } ) ,
2112+ serverUrl : new URL ( 'https://api.example.com/mcp-server' ) ,
2113+ fetchFn : mockFetch ,
2114+ accumulatedScope : 'read:op1 read:op2'
2115+ } )
2116+ ) . rejects . toThrow ( 'Unauthorized' ) ;
2117+
2118+ const redirectCall = vi . mocked ( mockProvider . redirectToAuthorization ) . mock . calls [ 0 ] ?. [ 0 ] ;
2119+ expect ( redirectCall ) . toBeInstanceOf ( URL ) ;
2120+ expect ( redirectCall ?. searchParams . get ( 'scope' ) ?. split ( ' ' ) . toSorted ( ) ) . toEqual ( [ 'read:op1' , 'read:op2' ] ) ;
2121+ } ) ;
2122+
20622123 it ( 'performs client_credentials with private_key_jwt when provider has addClientAuthentication' , async ( ) => {
20632124 // Arrange: metadata discovery for PRM and AS
20642125 mockFetch . mockImplementation ( url => {
0 commit comments