@@ -2366,4 +2366,94 @@ describe('(GHSA-c442-97qw-j6c6) SQL Injection via $regex query operator field na
23662366 }
23672367 } ) ;
23682368 } ) ;
2369+
2370+ describe ( '(GHSA-2cjm-2gwv-m892) OAuth2 adapter singleton shares mutable state across providers' , ( ) => {
2371+ it ( 'should return isolated adapter instances for different OAuth2 providers' , ( ) => {
2372+ const { loadAuthAdapter } = require ( '../lib/Adapters/Auth/index' ) ;
2373+
2374+ const authOptions = {
2375+ providerA : {
2376+ oauth2 : true ,
2377+ tokenIntrospectionEndpointUrl : 'https://a.example.com/introspect' ,
2378+ useridField : 'sub' ,
2379+ appidField : 'aud' ,
2380+ appIds : [ 'appA' ] ,
2381+ } ,
2382+ providerB : {
2383+ oauth2 : true ,
2384+ tokenIntrospectionEndpointUrl : 'https://b.example.com/introspect' ,
2385+ useridField : 'sub' ,
2386+ appidField : 'aud' ,
2387+ appIds : [ 'appB' ] ,
2388+ } ,
2389+ } ;
2390+
2391+ const resultA = loadAuthAdapter ( 'providerA' , authOptions ) ;
2392+ const resultB = loadAuthAdapter ( 'providerB' , authOptions ) ;
2393+
2394+ // Adapters must be different instances to prevent cross-contamination
2395+ expect ( resultA . adapter ) . not . toBe ( resultB . adapter ) ;
2396+
2397+ // After loading providerB, providerA's config must still be intact
2398+ expect ( resultA . adapter . tokenIntrospectionEndpointUrl ) . toBe ( 'https://a.example.com/introspect' ) ;
2399+ expect ( resultA . adapter . appIds ) . toEqual ( [ 'appA' ] ) ;
2400+ expect ( resultB . adapter . tokenIntrospectionEndpointUrl ) . toBe ( 'https://b.example.com/introspect' ) ;
2401+ expect ( resultB . adapter . appIds ) . toEqual ( [ 'appB' ] ) ;
2402+ } ) ;
2403+
2404+ it ( 'should not allow concurrent OAuth2 auth requests to cross-contaminate provider config' , async ( ) => {
2405+ await reconfigureServer ( {
2406+ auth : {
2407+ oauthProviderA : {
2408+ oauth2 : true ,
2409+ tokenIntrospectionEndpointUrl : 'https://a.example.com/introspect' ,
2410+ useridField : 'sub' ,
2411+ appidField : 'aud' ,
2412+ appIds : [ 'appA' ] ,
2413+ } ,
2414+ oauthProviderB : {
2415+ oauth2 : true ,
2416+ tokenIntrospectionEndpointUrl : 'https://b.example.com/introspect' ,
2417+ useridField : 'sub' ,
2418+ appidField : 'aud' ,
2419+ appIds : [ 'appB' ] ,
2420+ } ,
2421+ } ,
2422+ } ) ;
2423+
2424+ // Provider A: valid token with appA audience
2425+ // Provider B: valid token with appB audience
2426+ mockFetch ( [
2427+ {
2428+ url : 'https://a.example.com/introspect' ,
2429+ method : 'POST' ,
2430+ response : {
2431+ ok : true ,
2432+ json : ( ) => Promise . resolve ( { active : true , sub : 'user1' , aud : 'appA' } ) ,
2433+ } ,
2434+ } ,
2435+ {
2436+ url : 'https://b.example.com/introspect' ,
2437+ method : 'POST' ,
2438+ response : {
2439+ ok : true ,
2440+ json : ( ) => Promise . resolve ( { active : true , sub : 'user2' , aud : 'appB' } ) ,
2441+ } ,
2442+ } ,
2443+ ] ) ;
2444+
2445+ // Both providers should authenticate independently without cross-contamination
2446+ const [ userA , userB ] = await Promise . all ( [
2447+ Parse . User . logInWith ( 'oauthProviderA' , {
2448+ authData : { id : 'user1' , access_token : 'tokenA' } ,
2449+ } ) ,
2450+ Parse . User . logInWith ( 'oauthProviderB' , {
2451+ authData : { id : 'user2' , access_token : 'tokenB' } ,
2452+ } ) ,
2453+ ] ) ;
2454+
2455+ expect ( userA . id ) . toBeDefined ( ) ;
2456+ expect ( userB . id ) . toBeDefined ( ) ;
2457+ } ) ;
2458+ } ) ;
23692459} ) ;
0 commit comments