diff --git a/examples/clients/typescript/everything-client.ts b/examples/clients/typescript/everything-client.ts index 93fd142..21804a8 100644 --- a/examples/clients/typescript/everything-client.ts +++ b/examples/clients/typescript/everything-client.ts @@ -361,14 +361,165 @@ export async function runPreRegistration(serverUrl: string): Promise { 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: Complete Flow (SEP-990) + * Tests the complete flow: IDP ID token -> authorization grant -> access token -> MCP access. + */ +export async function runCrossAppAccessCompleteFlow( + serverUrl: string +): Promise { + 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); + + // Step 0: Discover resource and auth server from PRM metadata + logger.debug('Step 0: Discovering resource and auth server via PRM...'); + const prmUrl = new URL( + '/.well-known/oauth-protected-resource/mcp', + serverUrl + ); + const prmResponse = await fetch(prmUrl.toString()); + if (!prmResponse.ok) { + throw new Error(`PRM discovery failed: ${prmResponse.status}`); + } + const prm = await prmResponse.json(); + const resource = prm.resource; + const authServerUrl = prm.authorization_servers[0]; + logger.debug('Discovered resource:', resource); + logger.debug('Discovered auth server:', authServerUrl); + + // Discover auth server metadata to find token endpoint + const asMetadataUrl = new URL( + '/.well-known/oauth-authorization-server', + authServerUrl + ); + const asMetadataResponse = await fetch(asMetadataUrl.toString()); + if (!asMetadataResponse.ok) { + throw new Error( + `Auth server metadata discovery failed: ${asMetadataResponse.status}` + ); + } + const asMetadata = await asMetadataResponse.json(); + const asTokenEndpoint = asMetadata.token_endpoint; + const asIssuer = asMetadata.issuer; + logger.debug('Auth server issuer:', asIssuer); + logger.debug('Auth server token endpoint:', asTokenEndpoint); + + // Verify AS supports jwt-bearer grant type + const grantTypes: string[] = asMetadata.grant_types_supported || []; + if (!grantTypes.includes('urn:ietf:params:oauth:grant-type:jwt-bearer')) { + throw new Error( + `Auth server does not support jwt-bearer grant type. Supported: ${grantTypes.join(', ')}` + ); + } + logger.debug('Auth server supports jwt-bearer grant type'); + + // Step 1: Token Exchange at IdP (IDP ID token -> ID-JAG) + logger.debug('Step 1: Exchanging IDP ID token for ID-JAG at IdP...'); + const tokenExchangeParams = new URLSearchParams({ + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + requested_token_type: 'urn:ietf:params:oauth:token-type:id-jag', + audience: asIssuer, + resource: resource, + subject_token: ctx.idp_id_token, + subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', + 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 at AS (ID-JAG -> access token) + // Client authenticates via client_secret_basic (RFC 7523 Section 5) + 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 + }); + + const basicAuth = Buffer.from( + `${encodeURIComponent(ctx.client_id)}:${encodeURIComponent(ctx.client_secret)}` + ).toString('base64'); + + const tokenResponse = await fetch(asTokenEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${basicAuth}` + }, + 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 diff --git a/src/scenarios/client/auth/cross-app-access.ts b/src/scenarios/client/auth/cross-app-access.ts new file mode 100644 index 0000000..077889d --- /dev/null +++ b/src/scenarios/client/auth/cross-app-access.ts @@ -0,0 +1,550 @@ +import * as jose from 'jose'; +import type { CryptoKey } from 'jose'; +import express, { type Request, type Response } from 'express'; +import type { Scenario, ConformanceCheck, ScenarioUrls } from '../../../types'; +import { createAuthServer } from './helpers/createAuthServer'; +import { createServer } from './helpers/createServer'; +import { MockTokenVerifier } from './helpers/mockTokenVerifier'; +import { ServerLifecycle } from './helpers/serverLifecycle'; +import { SpecReferences } from './spec-references'; + +const CONFORMANCE_TEST_CLIENT_ID = 'conformance-test-xaa-client'; +const CONFORMANCE_TEST_CLIENT_SECRET = 'conformance-test-xaa-secret'; +const IDP_CLIENT_ID = 'conformance-test-idp-client'; +const DEMO_USER_ID = 'demo-user@example.com'; + +/** + * Generate an EC P-256 keypair for IDP ID token signing. + */ +async function generateIdpKeypair(): Promise<{ + publicKey: CryptoKey; + privateKey: CryptoKey; +}> { + const { publicKey, privateKey } = await jose.generateKeyPair('ES256', { + extractable: true + }); + return { publicKey, privateKey }; +} + +/** + * Create a signed ID token from the IDP + */ +async function createIdpIdToken( + privateKey: CryptoKey, + idpIssuer: string, + audience: string, + userId: string = DEMO_USER_ID +): Promise { + return await new jose.SignJWT({ + sub: userId, + email: userId, + aud: audience + }) + .setProtectedHeader({ alg: 'ES256' }) + .setIssuer(idpIssuer) + .setIssuedAt() + .setExpirationTime('1h') + .sign(privateKey); +} + +/** + * Scenario: Complete Cross-App Access Flow + * + * Tests the complete SEP-990 flow: IDP ID token -> authorization grant -> access token + * This scenario combines both RFC 8693 token exchange and RFC 7523 JWT bearer grant. + */ +export class CrossAppAccessCompleteFlowScenario implements Scenario { + name = 'auth/cross-app-access-complete-flow'; + description = + 'Tests complete SEP-990 flow: token exchange + JWT bearer grant (Enterprise Managed OAuth)'; + + private idpServer = new ServerLifecycle(); + private authServer = new ServerLifecycle(); + private mcpServer = new ServerLifecycle(); + private checks: ConformanceCheck[] = []; + private idpPublicKey?: CryptoKey; + private idpPrivateKey?: CryptoKey; + private grantKeypairs: Map = new Map(); + + async start(): Promise { + this.checks = []; + + // Generate IDP keypair + const { publicKey, privateKey } = await generateIdpKeypair(); + this.idpPublicKey = publicKey; + this.idpPrivateKey = privateKey; + + // Shared token verifier ensures MCP server only accepts tokens + // actually issued by the auth server + const tokenVerifier = new MockTokenVerifier(this.checks, []); + + // Start IDP server + await this.startIdpServer(); + + // Start auth server with JWT bearer grant support only + // Token exchange is handled by IdP + const authApp = createAuthServer(this.checks, this.authServer.getUrl, { + grantTypesSupported: ['urn:ietf:params:oauth:grant-type:jwt-bearer'], + tokenEndpointAuthMethodsSupported: ['client_secret_basic'], + tokenVerifier, + onTokenRequest: async ({ + grantType, + body, + timestamp, + authBaseUrl, + authorizationHeader + }) => { + // Auth server only handles JWT bearer grant (ID-JAG -> access token) + if (grantType === 'urn:ietf:params:oauth:grant-type:jwt-bearer') { + const mcpResourceUrl = `${this.mcpServer.getUrl()}/mcp`; + return await this.handleJwtBearerGrant( + body, + timestamp, + authBaseUrl, + authorizationHeader, + mcpResourceUrl + ); + } + + return { + error: 'unsupported_grant_type', + errorDescription: `Auth server only supports jwt-bearer grant, got ${grantType}` + }; + } + }); + + await this.authServer.start(authApp); + + // Start MCP server with shared token verifier + const mcpApp = createServer( + this.checks, + this.mcpServer.getUrl, + this.authServer.getUrl, + { tokenVerifier } + ); + + await this.mcpServer.start(mcpApp); + + // Generate IDP ID token for client + const idpIdToken = await createIdpIdToken( + this.idpPrivateKey!, + this.idpServer.getUrl(), + IDP_CLIENT_ID + ); + + return { + serverUrl: `${this.mcpServer.getUrl()}/mcp`, + context: { + client_id: CONFORMANCE_TEST_CLIENT_ID, + client_secret: CONFORMANCE_TEST_CLIENT_SECRET, + idp_client_id: IDP_CLIENT_ID, + idp_id_token: idpIdToken, + idp_issuer: this.idpServer.getUrl(), + idp_token_endpoint: `${this.idpServer.getUrl()}/token` + } + }; + } + + private async startIdpServer(): Promise { + const app = express(); + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + + // IDP metadata endpoint + app.get( + '/.well-known/openid-configuration', + (req: Request, res: Response) => { + res.json({ + issuer: this.idpServer.getUrl(), + authorization_endpoint: `${this.idpServer.getUrl()}/authorize`, + token_endpoint: `${this.idpServer.getUrl()}/token`, + jwks_uri: `${this.idpServer.getUrl()}/.well-known/jwks.json`, + grant_types_supported: [ + 'urn:ietf:params:oauth:grant-type:token-exchange' + ] + }); + } + ); + + // IDP token endpoint - handles token exchange (IDP ID token -> ID-JAG) + app.post('/token', async (req: Request, res: Response) => { + const timestamp = new Date().toISOString(); + const grantType = req.body.grant_type; + const subjectToken = req.body.subject_token; + const subjectTokenType = req.body.subject_token_type; + const requestedTokenType = req.body.requested_token_type; + const audience = req.body.audience; + const resource = req.body.resource; + + // Only handle token exchange at IdP + if (grantType !== 'urn:ietf:params:oauth:grant-type:token-exchange') { + this.checks.push({ + id: 'complete-flow-token-exchange', + name: 'CompleteFlowTokenExchange', + description: `IdP expected token-exchange grant, got ${grantType}`, + status: 'FAILURE', + timestamp, + specReferences: [SpecReferences.RFC_8693_TOKEN_EXCHANGE] + }); + res.status(400).json({ + error: 'unsupported_grant_type', + error_description: 'IdP only supports token-exchange' + }); + return; + } + + // Verify all required token exchange parameters per SEP-990 + const missingParams: string[] = []; + if (!subjectToken) missingParams.push('subject_token'); + if (subjectTokenType !== 'urn:ietf:params:oauth:token-type:id_token') { + missingParams.push( + `subject_token_type (expected urn:ietf:params:oauth:token-type:id_token, got ${subjectTokenType || 'missing'})` + ); + } + if (requestedTokenType !== 'urn:ietf:params:oauth:token-type:id-jag') { + missingParams.push( + `requested_token_type (expected urn:ietf:params:oauth:token-type:id-jag, got ${requestedTokenType || 'missing'})` + ); + } + if (!audience) missingParams.push('audience'); + if (!resource) missingParams.push('resource'); + + if (missingParams.length > 0) { + this.checks.push({ + id: 'complete-flow-token-exchange', + name: 'CompleteFlowTokenExchange', + description: `Token exchange missing or invalid required parameters: ${missingParams.join(', ')}`, + status: 'FAILURE', + timestamp, + specReferences: [ + SpecReferences.RFC_8693_TOKEN_EXCHANGE, + SpecReferences.SEP_990_ENTERPRISE_OAUTH + ] + }); + res.status(400).json({ + error: 'invalid_request', + error_description: `Missing or invalid required parameters: ${missingParams.join(', ')}` + }); + return; + } + + try { + // Verify the IDP ID token + const { payload } = await jose.jwtVerify( + subjectToken, + this.idpPublicKey!, + { + audience: IDP_CLIENT_ID, + issuer: this.idpServer.getUrl() + } + ); + + this.checks.push({ + id: 'complete-flow-token-exchange', + name: 'CompleteFlowTokenExchange', + description: + 'Successfully exchanged IDP ID token for ID-JAG at IdP with all required parameters', + status: 'SUCCESS', + timestamp, + specReferences: [ + SpecReferences.RFC_8693_TOKEN_EXCHANGE, + SpecReferences.SEP_990_ENTERPRISE_OAUTH + ] + }); + + // Create ID-JAG (ID-bound JSON Assertion Grant) + // Include resource and client_id claims per SEP-990 + const userId = payload.sub as string; + const { publicKey, privateKey } = await jose.generateKeyPair('ES256'); + this.grantKeypairs.set(userId, publicKey); + + // The IdP uses CONFORMANCE_TEST_CLIENT_ID (the MCP Client's client_id + // at the AS), not the IdP client_id from the request body. + // Per Section 6.1: "the IdP will need to be aware of the MCP Client's + // client_id that it normally uses with the MCP Server." + const idJag = await new jose.SignJWT({ + sub: userId, + resource: resource, + client_id: CONFORMANCE_TEST_CLIENT_ID + }) + .setProtectedHeader({ alg: 'ES256', typ: 'oauth-id-jag+jwt' }) + .setIssuer(this.idpServer.getUrl()) + .setAudience(audience) + .setIssuedAt() + .setExpirationTime('5m') + .setJti(crypto.randomUUID()) + .sign(privateKey); + + res.json({ + access_token: idJag, + issued_token_type: 'urn:ietf:params:oauth:token-type:id-jag', + token_type: 'N_A' + }); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + this.checks.push({ + id: 'complete-flow-token-exchange', + name: 'CompleteFlowTokenExchange', + description: `Token exchange failed: ${errorMessage}`, + status: 'FAILURE', + timestamp, + specReferences: [SpecReferences.RFC_8693_TOKEN_EXCHANGE] + }); + res.status(400).json({ + error: 'invalid_grant', + error_description: 'Invalid ID token' + }); + } + }); + + await this.idpServer.start(app); + } + + private async handleJwtBearerGrant( + body: Record, + timestamp: string, + authBaseUrl: string, + authorizationHeader?: string, + mcpResourceUrl?: string + ): Promise { + // 1. Verify client authentication (client_secret_basic) + if (!authorizationHeader || !authorizationHeader.startsWith('Basic ')) { + this.checks.push({ + id: 'complete-flow-jwt-bearer', + name: 'CompleteFlowJwtBearer', + description: + 'Missing or invalid Authorization header for client_secret_basic authentication', + status: 'FAILURE', + timestamp, + specReferences: [SpecReferences.SEP_990_ENTERPRISE_OAUTH], + details: { + expected: 'Authorization: Basic ', + received: authorizationHeader || 'missing' + } + }); + return { + error: 'invalid_client', + errorDescription: + 'Client authentication required (client_secret_basic)', + statusCode: 401 + }; + } + + const base64Credentials = authorizationHeader.slice('Basic '.length); + const decoded = Buffer.from(base64Credentials, 'base64').toString('utf-8'); + const separatorIndex = decoded.indexOf(':'); + if (separatorIndex === -1) { + this.checks.push({ + id: 'complete-flow-jwt-bearer', + name: 'CompleteFlowJwtBearer', + description: 'Malformed Basic auth header (no colon separator)', + status: 'FAILURE', + timestamp, + specReferences: [SpecReferences.SEP_990_ENTERPRISE_OAUTH] + }); + return { + error: 'invalid_client', + errorDescription: 'Malformed Basic auth', + statusCode: 401 + }; + } + + const authClientId = decodeURIComponent(decoded.slice(0, separatorIndex)); + const authClientSecret = decodeURIComponent( + decoded.slice(separatorIndex + 1) + ); + + if ( + authClientId !== CONFORMANCE_TEST_CLIENT_ID || + authClientSecret !== CONFORMANCE_TEST_CLIENT_SECRET + ) { + this.checks.push({ + id: 'complete-flow-jwt-bearer', + name: 'CompleteFlowJwtBearer', + description: `Client authentication failed: invalid credentials (client_id: ${authClientId})`, + status: 'FAILURE', + timestamp, + specReferences: [SpecReferences.SEP_990_ENTERPRISE_OAUTH] + }); + return { + error: 'invalid_client', + errorDescription: 'Invalid client credentials', + statusCode: 401 + }; + } + + // 2. Verify assertion is present + const assertion = body.assertion; + if (!assertion) { + this.checks.push({ + id: 'complete-flow-jwt-bearer', + name: 'CompleteFlowJwtBearer', + description: 'Missing assertion in JWT bearer grant', + status: 'FAILURE', + timestamp, + specReferences: [SpecReferences.RFC_7523_JWT_BEARER] + }); + return { + error: 'invalid_request', + errorDescription: 'Missing assertion' + }; + } + + try { + // 3. Verify the ID-JAG header has the correct typ + const header = jose.decodeProtectedHeader(assertion); + if (header.typ !== 'oauth-id-jag+jwt') { + this.checks.push({ + id: 'complete-flow-jwt-bearer', + name: 'CompleteFlowJwtBearer', + description: `ID-JAG has wrong typ header: expected oauth-id-jag+jwt, got ${header.typ}`, + status: 'FAILURE', + timestamp, + specReferences: [SpecReferences.SEP_990_ENTERPRISE_OAUTH] + }); + return { + error: 'invalid_grant', + errorDescription: 'Invalid ID-JAG typ header' + }; + } + + // 4. Decode and verify the ID-JAG + const decoded = jose.decodeJwt(assertion); + const userId = decoded.sub as string; + const publicKey = this.grantKeypairs.get(userId); + + if (!publicKey) { + throw new Error('Unknown authorization grant'); + } + + // Verify signature and audience + const withoutSlash = authBaseUrl.replace(/\/+$/, ''); + const withSlash = `${withoutSlash}/`; + + await jose.jwtVerify(assertion, publicKey, { + audience: [withoutSlash, withSlash], + clockTolerance: 30 + }); + + // 5. Verify client_id in ID-JAG matches the authenticating client (Section 5.1) + const jagClientId = decoded.client_id as string | undefined; + if (jagClientId !== authClientId) { + this.checks.push({ + id: 'complete-flow-jwt-bearer', + name: 'CompleteFlowJwtBearer', + description: `ID-JAG client_id (${jagClientId}) does not match authenticating client (${authClientId})`, + status: 'FAILURE', + timestamp, + specReferences: [SpecReferences.SEP_990_ENTERPRISE_OAUTH], + details: { + jagClientId, + authClientId + } + }); + return { + error: 'invalid_grant', + errorDescription: + 'ID-JAG client_id does not match authenticating client' + }; + } + + // 6. Verify resource claim in ID-JAG matches the MCP server resource + const jagResource = decoded.resource as string | undefined; + if (mcpResourceUrl && jagResource !== mcpResourceUrl) { + this.checks.push({ + id: 'complete-flow-jwt-bearer', + name: 'CompleteFlowJwtBearer', + description: `ID-JAG resource (${jagResource}) does not match MCP server resource (${mcpResourceUrl})`, + status: 'FAILURE', + timestamp, + specReferences: [SpecReferences.SEP_990_ENTERPRISE_OAUTH], + details: { + jagResource, + expectedResource: mcpResourceUrl + } + }); + return { + error: 'invalid_grant', + errorDescription: 'ID-JAG resource does not match MCP server resource' + }; + } + + this.checks.push({ + id: 'complete-flow-jwt-bearer', + name: 'CompleteFlowJwtBearer', + description: + 'Successfully verified client auth, ID-JAG claims, and exchanged for access token', + status: 'SUCCESS', + timestamp, + specReferences: [ + SpecReferences.RFC_7523_JWT_BEARER, + SpecReferences.SEP_990_ENTERPRISE_OAUTH + ] + }); + + const scopes = body.scope ? body.scope.split(' ') : []; + return { + token: `test-token-${Date.now()}`, + scopes + }; + } catch (e) { + this.checks.push({ + id: 'complete-flow-jwt-bearer', + name: 'CompleteFlowJwtBearer', + description: `JWT bearer grant failed: ${e}`, + status: 'FAILURE', + timestamp, + specReferences: [SpecReferences.RFC_7523_JWT_BEARER] + }); + return { + error: 'invalid_grant', + errorDescription: 'Invalid authorization grant' + }; + } + } + + async stop() { + await this.idpServer.stop(); + await this.authServer.stop(); + await this.mcpServer.stop(); + } + + getChecks(): ConformanceCheck[] { + const hasTokenExchangeCheck = this.checks.some( + (c) => c.id === 'complete-flow-token-exchange' + ); + const hasJwtBearerCheck = this.checks.some( + (c) => c.id === 'complete-flow-jwt-bearer' + ); + + if (!hasTokenExchangeCheck) { + this.checks.push({ + id: 'complete-flow-token-exchange', + name: 'CompleteFlowTokenExchange', + description: 'Client did not perform token exchange', + status: 'FAILURE', + timestamp: new Date().toISOString(), + specReferences: [ + SpecReferences.RFC_8693_TOKEN_EXCHANGE, + SpecReferences.SEP_990_ENTERPRISE_OAUTH + ] + }); + } + + if (!hasJwtBearerCheck) { + this.checks.push({ + id: 'complete-flow-jwt-bearer', + name: 'CompleteFlowJwtBearer', + description: 'Client did not perform JWT bearer grant exchange', + status: 'FAILURE', + timestamp: new Date().toISOString(), + specReferences: [ + SpecReferences.RFC_7523_JWT_BEARER, + SpecReferences.SEP_990_ENTERPRISE_OAUTH + ] + }); + } + + return this.checks; + } +} diff --git a/src/scenarios/client/auth/index.ts b/src/scenarios/client/auth/index.ts index 7f75113..73c9ddb 100644 --- a/src/scenarios/client/auth/index.ts +++ b/src/scenarios/client/auth/index.ts @@ -23,6 +23,7 @@ import { } from './client-credentials'; import { ResourceMismatchScenario } from './resource-mismatch'; import { PreRegistrationScenario } from './pre-registration'; +import { CrossAppAccessCompleteFlowScenario } from './cross-app-access'; // Auth scenarios (required for tier 1) export const authScenariosList: Scenario[] = [ @@ -49,5 +50,6 @@ export const backcompatScenariosList: Scenario[] = [ // Extension scenarios (optional for tier 1 - protocol extensions) export const extensionScenariosList: Scenario[] = [ new ClientCredentialsJwtScenario(), - new ClientCredentialsBasicScenario() + new ClientCredentialsBasicScenario(), + new CrossAppAccessCompleteFlowScenario() ]; diff --git a/src/scenarios/client/auth/spec-references.ts b/src/scenarios/client/auth/spec-references.ts index a24e987..4020bfc 100644 --- a/src/scenarios/client/auth/spec-references.ts +++ b/src/scenarios/client/auth/spec-references.ts @@ -88,5 +88,17 @@ export const SpecReferences: { [key: string]: SpecReference } = { MCP_PKCE: { id: 'MCP-PKCE-requirement', url: 'https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#authorization-code-protection' + }, + RFC_8693_TOKEN_EXCHANGE: { + id: 'RFC-8693-Token-Exchange', + url: 'https://datatracker.ietf.org/doc/html/rfc8693' + }, + RFC_7523_JWT_BEARER: { + id: 'RFC-7523-JWT-Bearer-Grant', + url: 'https://datatracker.ietf.org/doc/html/rfc7523' + }, + SEP_990_ENTERPRISE_OAUTH: { + id: 'SEP-990-Enterprise-Managed-OAuth', + url: 'https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-oauth.mdx' } }; diff --git a/src/schemas/context.ts b/src/schemas/context.ts index 2a8a907..9a5e249 100644 --- a/src/schemas/context.ts +++ b/src/schemas/context.ts @@ -22,6 +22,15 @@ export const ClientConformanceContextSchema = z.discriminatedUnion('name', [ name: z.literal('auth/pre-registration'), client_id: z.string(), client_secret: z.string() + }), + z.object({ + name: z.literal('auth/cross-app-access-complete-flow'), + client_id: z.string(), + client_secret: z.string(), + idp_client_id: z.string(), + idp_id_token: z.string(), + idp_issuer: z.string(), + idp_token_endpoint: z.string() }) ]);