@@ -337,6 +337,42 @@ describe('OAuth2Adapter', () => {
337337 ) ;
338338 } ) ;
339339
340+ it ( 'should send the correct access token to the introspection endpoint during app ID validation' , async ( ) => {
341+ const capturedTokens = [ ] ;
342+ const originalFetch = global . fetch ;
343+ try {
344+ global . fetch = async ( url , options ) => {
345+ if ( typeof url === 'string' && url === 'https://provider.com/introspect' ) {
346+ const body = options ?. body ?. toString ( ) || '' ;
347+ const token = new URLSearchParams ( body ) . get ( 'token' ) ;
348+ capturedTokens . push ( token ) ;
349+ return {
350+ ok : true ,
351+ json : ( ) => Promise . resolve ( {
352+ active : true ,
353+ sub : 'user123' ,
354+ aud : 'valid-app-id' ,
355+ } ) ,
356+ } ;
357+ }
358+ return originalFetch ( url , options ) ;
359+ } ;
360+
361+ const authData = { access_token : 'myRealAccessToken' , id : 'user123' } ;
362+ const user = await Parse . User . logInWith ( 'mockOauth' , { authData } ) ;
363+ expect ( user . id ) . toBeDefined ( ) ;
364+
365+ // With appidField configured, validateAppId and validateAuthData both call requestTokenInfo.
366+ // Both should receive the actual access token, not 'undefined' from argument mismatch.
367+ expect ( capturedTokens . length ) . toBeGreaterThanOrEqual ( 2 ) ;
368+ for ( const token of capturedTokens ) {
369+ expect ( token ) . toBe ( 'myRealAccessToken' ) ;
370+ }
371+ } finally {
372+ global . fetch = originalFetch ;
373+ }
374+ } ) ;
375+
340376 it ( 'should reject account takeover when useridField is omitted and attacker uses their own token with victim ID' , async ( ) => {
341377 await reconfigureServer ( {
342378 auth : {
0 commit comments