Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 223 additions & 2 deletions examples/clients/typescript/everything-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,14 +361,235 @@ export async function runPreRegistration(serverUrl: string): Promise<void> {
await client.listTools();
logger.debug('Successfully listed tools');

await transport.close();
logger.debug('Connection closed successfully');
}

registerScenario('auth/pre-registration', runPreRegistration);

// ============================================================================
// Cross-App Access (SEP-990) scenarios
// ============================================================================

/**
* Cross-app access: Token Exchange (RFC 8693)
* Tests the first step of SEP-990 where IDP ID token is exchanged for authorization grant.
*/
export async function runCrossAppAccessTokenExchange(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these other scenarios are unused, please delete

_serverUrl: string
): Promise<void> {
const ctx = parseContext();
if (ctx.name !== 'auth/cross-app-access-token-exchange') {
throw new Error(
`Expected cross-app-access-token-exchange context, got ${ctx.name}`
);
}

logger.debug('Starting token exchange flow...');
logger.debug('IDP Issuer:', ctx.idp_issuer);
logger.debug('Auth Server:', ctx.auth_server_url);

// Step 1: Exchange IDP ID token for authorization grant using RFC 8693
const tokenExchangeParams = new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
subject_token: ctx.idp_id_token,
subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
client_id: ctx.client_id
});

logger.debug('Performing token exchange...');
const tokenExchangeResponse = await fetch(`${ctx.auth_server_url}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: tokenExchangeParams
});

if (!tokenExchangeResponse.ok) {
const error = await tokenExchangeResponse.text();
throw new Error(`Token exchange failed: ${error}`);
}

const tokenExchangeResult = await tokenExchangeResponse.json();
logger.debug('Token exchange successful');
logger.debug('Issued token type:', tokenExchangeResult.issued_token_type);

// Note: In a real implementation, this authorization grant would be used
// in a subsequent JWT bearer grant flow to get an access token
logger.debug('Token exchange flow completed successfully');
}

registerScenario(
'auth/cross-app-access-token-exchange',
runCrossAppAccessTokenExchange
);

/**
* Cross-app access: JWT Bearer Grant (RFC 7523)
* Tests the second step of SEP-990 where authorization grant is exchanged for access token.
*/
export async function runCrossAppAccessJwtBearer(
serverUrl: string
): Promise<void> {
const ctx = parseContext();
if (ctx.name !== 'auth/cross-app-access-jwt-bearer') {
throw new Error(
`Expected cross-app-access-jwt-bearer context, got ${ctx.name}`
);
}

logger.debug('Starting JWT bearer grant flow...');
logger.debug('Auth Server:', ctx.auth_server_url);

// Exchange authorization grant for access token using RFC 7523
const jwtBearerParams = new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: ctx.authorization_grant,
client_id: ctx.client_id
});

logger.debug('Performing JWT bearer grant...');
const tokenResponse = await fetch(`${ctx.auth_server_url}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: jwtBearerParams
});

if (!tokenResponse.ok) {
const error = await tokenResponse.text();
throw new Error(`JWT bearer grant failed: ${error}`);
}

const tokenResult = await tokenResponse.json();
logger.debug('JWT bearer grant successful');
logger.debug('Access token obtained');

// Use the access token to connect to MCP server
const client = new Client(
{ name: 'conformance-cross-app-access', version: '1.0.0' },
{ capabilities: {} }
);

const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
requestInit: {
headers: {
Authorization: `Bearer ${tokenResult.access_token}`
}
}
});

await client.connect(transport);
logger.debug('Successfully connected to MCP server with access token');

await client.listTools();
logger.debug('Successfully listed tools');

await transport.close();
logger.debug('Connection closed successfully');
}

registerScenario(
'auth/cross-app-access-jwt-bearer',
runCrossAppAccessJwtBearer
);

/**
* Cross-app access: Complete Flow (SEP-990)
* Tests the complete flow: IDP ID token -> authorization grant -> access token -> MCP access.
*/
export async function runCrossAppAccessCompleteFlow(
serverUrl: string
): Promise<void> {
const ctx = parseContext();
if (ctx.name !== 'auth/cross-app-access-complete-flow') {
throw new Error(
`Expected cross-app-access-complete-flow context, got ${ctx.name}`
);
}

logger.debug('Starting complete cross-app access flow...');
logger.debug('IDP Issuer:', ctx.idp_issuer);
logger.debug('IDP Token Endpoint:', ctx.idp_token_endpoint);
logger.debug('Auth Server:', ctx.auth_server_url);

// Step 1: Token Exchange (IDP ID token -> ID-JAG)
logger.debug('Step 1: Exchanging IDP ID token for ID-JAG at IdP...');
const tokenExchangeParams = new URLSearchParams({
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-managed-authorization.mdx

Parameter Required/Optional Description Example/Allowed Values
requested_token_type REQUIRED Indicates that an ID Assertion JWT is being requested. urn:ietf:params:oauth:token-type:id-jag
audience REQUIRED The Issuer URL of the MCP server's authorization server. https://auth.chat.example/
resource REQUIRED The RFC9728 Resource Identifier of the MCP server. https://mcp.chat.example/
scope OPTIONAL The space-separated list of scopes at the MCP Server that are being requested. scope1 scope2
subject_token REQUIRED The identity assertion (e.g. the OpenID Connect ID Token or SAML assertion) for the target end-user. (JWT or SAML assertion string)
subject_token_type REQUIRED Indicates the type of the security token in the subject_token parameter, as specified in RFC8693 Section 3. urn:ietf:params:oauth:token-type:id_token (OIDC)urn:ietf:params:oauth:token-type:saml2 (SAML)

we're missing several required parameters here.

grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
subject_token: ctx.idp_id_token,
subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
client_id: ctx.client_id
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
client_id: ctx.client_id
client_id: ctx.idp_client_id

});

const tokenExchangeResponse = await fetch(ctx.idp_token_endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: tokenExchangeParams
});

if (!tokenExchangeResponse.ok) {
const error = await tokenExchangeResponse.text();
throw new Error(`Token exchange failed: ${error}`);
}

const tokenExchangeResult = await tokenExchangeResponse.json();
const idJag = tokenExchangeResult.access_token; // ID-JAG (ID-bound JSON Assertion Grant)
logger.debug('Token exchange successful, ID-JAG obtained');
logger.debug('Issued token type:', tokenExchangeResult.issued_token_type);

// Step 2: JWT Bearer Grant (ID-JAG -> access token)
logger.debug('Step 2: Exchanging ID-JAG for access token at Auth Server...');
const jwtBearerParams = new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: idJag,
client_id: ctx.client_id
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs to be a distinct client id from the one used for the IdP

});

const tokenResponse = await fetch(`${ctx.auth_server_url}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: jwtBearerParams
});

if (!tokenResponse.ok) {
const error = await tokenResponse.text();
throw new Error(`JWT bearer grant failed: ${error}`);
}

const tokenResult = await tokenResponse.json();
logger.debug('JWT bearer grant successful, access token obtained');

// Step 3: Use access token to access MCP server
logger.debug('Step 3: Accessing MCP server with access token...');
const client = new Client(
{ name: 'conformance-cross-app-access', version: '1.0.0' },
{ capabilities: {} }
);

const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
requestInit: {
headers: {
Authorization: `Bearer ${tokenResult.access_token}`
}
}
});

await client.connect(transport);
logger.debug('Successfully connected to MCP server');

await client.listTools();
logger.debug('Successfully listed tools');

await client.callTool({ name: 'test-tool', arguments: {} });
logger.debug('Successfully called tool');

await transport.close();
logger.debug('Connection closed successfully');
logger.debug('Complete cross-app access flow completed successfully');
}

registerScenario('auth/pre-registration', runPreRegistration);
registerScenario(
'auth/cross-app-access-complete-flow',
runCrossAppAccessCompleteFlow
);

// ============================================================================
// Main entry point
Expand Down
Loading
Loading