Skip to content

Commit 3348fd7

Browse files
committed
feat: add conformance tests for SEP-990
1 parent 189a31d commit 3348fd7

5 files changed

Lines changed: 1041 additions & 3 deletions

File tree

examples/clients/typescript/everything-client.ts

Lines changed: 216 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)