|
12 | 12 | * consolidating all the individual test clients into one. |
13 | 13 | */ |
14 | 14 |
|
15 | | -import { Client, ClientCredentialsProvider, PrivateKeyJwtProvider, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; |
| 15 | +import { |
| 16 | + Client, |
| 17 | + ClientCredentialsProvider, |
| 18 | + CrossAppAccessProvider, |
| 19 | + PrivateKeyJwtProvider, |
| 20 | + requestJwtAuthorizationGrant, |
| 21 | + StreamableHTTPClientTransport |
| 22 | +} from '@modelcontextprotocol/client'; |
16 | 23 | import * as z from 'zod/v4'; |
17 | 24 |
|
18 | 25 | import { logger } from './helpers/logger.js'; |
@@ -42,6 +49,15 @@ const ClientConformanceContextSchema = z.discriminatedUnion('name', [ |
42 | 49 | name: z.literal('auth/client-credentials-basic'), |
43 | 50 | client_id: z.string(), |
44 | 51 | client_secret: z.string() |
| 52 | + }), |
| 53 | + z.object({ |
| 54 | + name: z.literal('auth/cross-app-access-complete-flow'), |
| 55 | + client_id: z.string(), |
| 56 | + client_secret: z.string(), |
| 57 | + idp_client_id: z.string(), |
| 58 | + idp_id_token: z.string(), |
| 59 | + idp_issuer: z.string(), |
| 60 | + idp_token_endpoint: z.string() |
45 | 61 | }) |
46 | 62 | ]); |
47 | 63 |
|
@@ -240,6 +256,54 @@ async function runClientCredentialsBasic(serverUrl: string): Promise<void> { |
240 | 256 |
|
241 | 257 | registerScenario('auth/client-credentials-basic', runClientCredentialsBasic); |
242 | 258 |
|
| 259 | +/** |
| 260 | + * Cross-App Access (SEP-990 Enterprise Managed Authorization). |
| 261 | + * |
| 262 | + * Exchanges an IdP-issued ID token for an ID-JAG (RFC 8693 token exchange at the IdP), |
| 263 | + * then exchanges the ID-JAG for an access token at the AS (RFC 7523 JWT bearer grant |
| 264 | + * with client_secret_basic). The provider drives discovery + the JWT bearer step; the |
| 265 | + * assertion callback handles the IdP exchange using the context-supplied ID token. |
| 266 | + */ |
| 267 | +async function runCrossAppAccessCompleteFlow(serverUrl: string): Promise<void> { |
| 268 | + const ctx = parseContext(); |
| 269 | + if (ctx.name !== 'auth/cross-app-access-complete-flow') { |
| 270 | + throw new Error(`Expected cross-app-access context, got ${ctx.name}`); |
| 271 | + } |
| 272 | + |
| 273 | + const provider = new CrossAppAccessProvider({ |
| 274 | + clientId: ctx.client_id, |
| 275 | + clientSecret: ctx.client_secret, |
| 276 | + assertion: async authCtx => { |
| 277 | + const result = await requestJwtAuthorizationGrant({ |
| 278 | + tokenEndpoint: ctx.idp_token_endpoint, |
| 279 | + audience: authCtx.authorizationServerUrl, |
| 280 | + resource: authCtx.resourceUrl, |
| 281 | + idToken: ctx.idp_id_token, |
| 282 | + clientId: ctx.idp_client_id, |
| 283 | + fetchFn: authCtx.fetchFn |
| 284 | + }); |
| 285 | + return result.jwtAuthGrant; |
| 286 | + } |
| 287 | + }); |
| 288 | + |
| 289 | + const client = new Client({ name: 'conformance-cross-app-access', version: '1.0.0' }, { capabilities: {} }); |
| 290 | + |
| 291 | + const transport = new StreamableHTTPClientTransport(new URL(serverUrl), { |
| 292 | + authProvider: provider |
| 293 | + }); |
| 294 | + |
| 295 | + await client.connect(transport); |
| 296 | + logger.debug('Successfully connected with cross-app-access auth'); |
| 297 | + |
| 298 | + await client.listTools(); |
| 299 | + logger.debug('Successfully listed tools'); |
| 300 | + |
| 301 | + await transport.close(); |
| 302 | + logger.debug('Connection closed successfully'); |
| 303 | +} |
| 304 | + |
| 305 | +registerScenario('auth/cross-app-access-complete-flow', runCrossAppAccessCompleteFlow); |
| 306 | + |
243 | 307 | // ============================================================================ |
244 | 308 | // Elicitation defaults scenario |
245 | 309 | // ============================================================================ |
|
0 commit comments