@@ -361,14 +361,228 @@ export async function runPreRegistration(serverUrl: string): Promise<void> {
361361 await client . listTools ( ) ;
362362 logger . debug ( 'Successfully listed tools' ) ;
363363
364+ await transport . close ( ) ;
365+ logger . debug ( 'Connection closed successfully' ) ;
366+ }
367+
368+ registerScenario ( 'auth/pre-registration' , runPreRegistration ) ;
369+
370+ // ============================================================================
371+ // Cross-App Access (SEP-990) scenarios
372+ // ============================================================================
373+
374+ /**
375+ * Cross-app access: Token Exchange (RFC 8693)
376+ * Tests the first step of SEP-990 where IDP ID token is exchanged for authorization grant.
377+ */
378+ export async function runCrossAppAccessTokenExchange (
379+ serverUrl : string
380+ ) : Promise < void > {
381+ const ctx = parseContext ( ) ;
382+ if ( ctx . name !== 'auth/cross-app-access-token-exchange' ) {
383+ throw new Error (
384+ `Expected cross-app-access-token-exchange context, got ${ ctx . name } `
385+ ) ;
386+ }
387+
388+ logger . debug ( 'Starting token exchange flow...' ) ;
389+ logger . debug ( 'IDP Issuer:' , ctx . idp_issuer ) ;
390+ logger . debug ( 'Auth Server:' , ctx . auth_server_url ) ;
391+
392+ // Step 1: Exchange IDP ID token for authorization grant using RFC 8693
393+ const tokenExchangeParams = new URLSearchParams ( {
394+ grant_type : 'urn:ietf:params:oauth:grant-type:token-exchange' ,
395+ subject_token : ctx . idp_id_token ,
396+ subject_token_type : 'urn:ietf:params:oauth:token-type:id_token' ,
397+ client_id : ctx . client_id
398+ } ) ;
399+
400+ logger . debug ( 'Performing token exchange...' ) ;
401+ const tokenExchangeResponse = await fetch ( `${ ctx . auth_server_url } /token` , {
402+ method : 'POST' ,
403+ headers : { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
404+ body : tokenExchangeParams
405+ } ) ;
406+
407+ if ( ! tokenExchangeResponse . ok ) {
408+ const error = await tokenExchangeResponse . text ( ) ;
409+ throw new Error ( `Token exchange failed: ${ error } ` ) ;
410+ }
411+
412+ const tokenExchangeResult = await tokenExchangeResponse . json ( ) ;
413+ logger . debug ( 'Token exchange successful' ) ;
414+ logger . debug ( 'Issued token type:' , tokenExchangeResult . issued_token_type ) ;
415+
416+ // Note: In a real implementation, this authorization grant would be used
417+ // in a subsequent JWT bearer grant flow to get an access token
418+ logger . debug ( 'Token exchange flow completed successfully' ) ;
419+ }
420+
421+ registerScenario (
422+ 'auth/cross-app-access-token-exchange' ,
423+ runCrossAppAccessTokenExchange
424+ ) ;
425+
426+ /**
427+ * Cross-app access: JWT Bearer Grant (RFC 7523)
428+ * Tests the second step of SEP-990 where authorization grant is exchanged for access token.
429+ */
430+ export async function runCrossAppAccessJwtBearer (
431+ serverUrl : string
432+ ) : Promise < void > {
433+ const ctx = parseContext ( ) ;
434+ if ( ctx . name !== 'auth/cross-app-access-jwt-bearer' ) {
435+ throw new Error ( `Expected cross-app-access-jwt-bearer context, got ${ ctx . name } ` ) ;
436+ }
437+
438+ logger . debug ( 'Starting JWT bearer grant flow...' ) ;
439+ logger . debug ( 'Auth Server:' , ctx . auth_server_url ) ;
440+
441+ // Exchange authorization grant for access token using RFC 7523
442+ const jwtBearerParams = new URLSearchParams ( {
443+ grant_type : 'urn:ietf:params:oauth:grant-type:jwt-bearer' ,
444+ assertion : ctx . authorization_grant ,
445+ client_id : ctx . client_id
446+ } ) ;
447+
448+ logger . debug ( 'Performing JWT bearer grant...' ) ;
449+ const tokenResponse = await fetch ( `${ ctx . auth_server_url } /token` , {
450+ method : 'POST' ,
451+ headers : { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
452+ body : jwtBearerParams
453+ } ) ;
454+
455+ if ( ! tokenResponse . ok ) {
456+ const error = await tokenResponse . text ( ) ;
457+ throw new Error ( `JWT bearer grant failed: ${ error } ` ) ;
458+ }
459+
460+ const tokenResult = await tokenResponse . json ( ) ;
461+ logger . debug ( 'JWT bearer grant successful' ) ;
462+ logger . debug ( 'Access token obtained' ) ;
463+
464+ // Use the access token to connect to MCP server
465+ const client = new Client (
466+ { name : 'conformance-cross-app-access' , version : '1.0.0' } ,
467+ { capabilities : { } }
468+ ) ;
469+
470+ const transport = new StreamableHTTPClientTransport ( new URL ( serverUrl ) , {
471+ requestInit : {
472+ headers : {
473+ Authorization : `Bearer ${ tokenResult . access_token } `
474+ }
475+ }
476+ } ) ;
477+
478+ await client . connect ( transport ) ;
479+ logger . debug ( 'Successfully connected to MCP server with access token' ) ;
480+
481+ await client . listTools ( ) ;
482+ logger . debug ( 'Successfully listed tools' ) ;
483+
484+ await transport . close ( ) ;
485+ logger . debug ( 'Connection closed successfully' ) ;
486+ }
487+
488+ registerScenario ( 'auth/cross-app-access-jwt-bearer' , runCrossAppAccessJwtBearer ) ;
489+
490+ /**
491+ * Cross-app access: Complete Flow (SEP-990)
492+ * Tests the complete flow: IDP ID token -> authorization grant -> access token -> MCP access.
493+ */
494+ export async function runCrossAppAccessCompleteFlow (
495+ serverUrl : string
496+ ) : Promise < void > {
497+ const ctx = parseContext ( ) ;
498+ if ( ctx . name !== 'auth/cross-app-access-complete-flow' ) {
499+ throw new Error (
500+ `Expected cross-app-access-complete-flow context, got ${ ctx . name } `
501+ ) ;
502+ }
503+
504+ logger . debug ( 'Starting complete cross-app access flow...' ) ;
505+ logger . debug ( 'IDP Issuer:' , ctx . idp_issuer ) ;
506+ logger . debug ( 'Auth Server:' , ctx . auth_server_url ) ;
507+
508+ // Step 1: Token Exchange (IDP ID token -> authorization grant)
509+ logger . debug ( 'Step 1: Exchanging IDP ID token for authorization grant...' ) ;
510+ const tokenExchangeParams = new URLSearchParams ( {
511+ grant_type : 'urn:ietf:params:oauth:grant-type:token-exchange' ,
512+ subject_token : ctx . idp_id_token ,
513+ subject_token_type : 'urn:ietf:params:oauth:token-type:id_token' ,
514+ client_id : ctx . client_id
515+ } ) ;
516+
517+ const tokenExchangeResponse = await fetch ( `${ ctx . auth_server_url } /token` , {
518+ method : 'POST' ,
519+ headers : { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
520+ body : tokenExchangeParams
521+ } ) ;
522+
523+ if ( ! tokenExchangeResponse . ok ) {
524+ const error = await tokenExchangeResponse . text ( ) ;
525+ throw new Error ( `Token exchange failed: ${ error } ` ) ;
526+ }
527+
528+ const tokenExchangeResult = await tokenExchangeResponse . json ( ) ;
529+ const authorizationGrant = tokenExchangeResult . access_token ;
530+ logger . debug ( 'Token exchange successful, authorization grant obtained' ) ;
531+
532+ // Step 2: JWT Bearer Grant (authorization grant -> access token)
533+ logger . debug ( 'Step 2: Exchanging authorization grant for access token...' ) ;
534+ const jwtBearerParams = new URLSearchParams ( {
535+ grant_type : 'urn:ietf:params:oauth:grant-type:jwt-bearer' ,
536+ assertion : authorizationGrant ,
537+ client_id : ctx . client_id
538+ } ) ;
539+
540+ const tokenResponse = await fetch ( `${ ctx . auth_server_url } /token` , {
541+ method : 'POST' ,
542+ headers : { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
543+ body : jwtBearerParams
544+ } ) ;
545+
546+ if ( ! tokenResponse . ok ) {
547+ const error = await tokenResponse . text ( ) ;
548+ throw new Error ( `JWT bearer grant failed: ${ error } ` ) ;
549+ }
550+
551+ const tokenResult = await tokenResponse . json ( ) ;
552+ logger . debug ( 'JWT bearer grant successful, access token obtained' ) ;
553+
554+ // Step 3: Use access token to access MCP server
555+ logger . debug ( 'Step 3: Accessing MCP server with access token...' ) ;
556+ const client = new Client (
557+ { name : 'conformance-cross-app-access' , version : '1.0.0' } ,
558+ { capabilities : { } }
559+ ) ;
560+
561+ const transport = new StreamableHTTPClientTransport ( new URL ( serverUrl ) , {
562+ requestInit : {
563+ headers : {
564+ Authorization : `Bearer ${ tokenResult . access_token } `
565+ }
566+ }
567+ } ) ;
568+
569+ await client . connect ( transport ) ;
570+ logger . debug ( 'Successfully connected to MCP server' ) ;
571+
572+ await client . listTools ( ) ;
573+ logger . debug ( 'Successfully listed tools' ) ;
574+
364575 await client . callTool ( { name : 'test-tool' , arguments : { } } ) ;
365576 logger . debug ( 'Successfully called tool' ) ;
366577
367578 await transport . close ( ) ;
368- logger . debug ( 'Connection closed successfully' ) ;
579+ logger . debug ( 'Complete cross-app access flow completed successfully' ) ;
369580}
370581
371- registerScenario ( 'auth/pre-registration' , runPreRegistration ) ;
582+ registerScenario (
583+ 'auth/cross-app-access-complete-flow' ,
584+ runCrossAppAccessCompleteFlow
585+ ) ;
372586
373587// ============================================================================
374588// Main entry point
0 commit comments