|
2 | 2 |
|
3 | 3 | import java.time.Duration; |
4 | 4 |
|
| 5 | +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
| 6 | +import com.fasterxml.jackson.annotation.JsonProperty; |
| 7 | +import com.fasterxml.jackson.databind.ObjectMapper; |
5 | 8 | import io.modelcontextprotocol.client.McpClient; |
6 | 9 | import io.modelcontextprotocol.client.McpSyncClient; |
| 10 | +import io.modelcontextprotocol.client.auth.DiscoverAndRequestJwtAuthGrantOptions; |
| 11 | +import io.modelcontextprotocol.client.auth.EnterpriseAuth; |
| 12 | +import io.modelcontextprotocol.client.auth.EnterpriseAuthProvider; |
| 13 | +import io.modelcontextprotocol.client.auth.EnterpriseAuthProviderOptions; |
7 | 14 | import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; |
8 | 15 | import io.modelcontextprotocol.spec.McpSchema; |
9 | 16 |
|
@@ -53,13 +60,17 @@ public static void main(String[] args) { |
53 | 60 | case "sse-retry": |
54 | 61 | runSSERetryScenario(serverUrl); |
55 | 62 | break; |
| 63 | + case "auth/cross-app-access-complete-flow": |
| 64 | + runCrossAppAccessCompleteFlowScenario(serverUrl); |
| 65 | + break; |
56 | 66 | default: |
57 | 67 | System.err.println("Unknown scenario: " + scenario); |
58 | 68 | System.err.println("Available scenarios:"); |
59 | 69 | System.err.println(" - initialize"); |
60 | 70 | System.err.println(" - tools_call"); |
61 | 71 | System.err.println(" - elicitation-sep1034-client-defaults"); |
62 | 72 | System.err.println(" - sse-retry"); |
| 73 | + System.err.println(" - auth/cross-app-access-complete-flow"); |
63 | 74 | System.exit(1); |
64 | 75 | } |
65 | 76 | System.exit(0); |
@@ -283,4 +294,82 @@ private static void runSSERetryScenario(String serverUrl) throws Exception { |
283 | 294 | } |
284 | 295 | } |
285 | 296 |
|
| 297 | + /** |
| 298 | + * Cross-App Access scenario: Tests SEP-990 Enterprise Managed Authorization flow. |
| 299 | + * <p> |
| 300 | + * Reads context from {@code MCP_CONFORMANCE_CONTEXT} (JSON) containing: |
| 301 | + * {@code client_id}, {@code client_secret}, {@code idp_client_id}, |
| 302 | + * {@code idp_id_token}, {@code idp_issuer}, {@code idp_token_endpoint}. |
| 303 | + * <p> |
| 304 | + * Uses {@link EnterpriseAuthProvider} with an assertion callback that performs RFC |
| 305 | + * 8693 token exchange at the IdP, then exchanges the ID-JAG for an access token at |
| 306 | + * the MCP authorization server via RFC 7523 JWT Bearer grant. |
| 307 | + * @param serverUrl the URL of the MCP server |
| 308 | + * @throws Exception if any error occurs during execution |
| 309 | + */ |
| 310 | + private static void runCrossAppAccessCompleteFlowScenario(String serverUrl) throws Exception { |
| 311 | + String contextEnv = System.getenv("MCP_CONFORMANCE_CONTEXT"); |
| 312 | + if (contextEnv == null || contextEnv.isEmpty()) { |
| 313 | + System.err.println("Error: MCP_CONFORMANCE_CONTEXT environment variable is not set"); |
| 314 | + System.exit(1); |
| 315 | + } |
| 316 | + |
| 317 | + CrossAppAccessContext ctx = new ObjectMapper().readValue(contextEnv, CrossAppAccessContext.class); |
| 318 | + |
| 319 | + java.net.http.HttpClient httpClient = java.net.http.HttpClient.newHttpClient(); |
| 320 | + |
| 321 | + EnterpriseAuthProviderOptions options = EnterpriseAuthProviderOptions.builder() |
| 322 | + .clientId(ctx.clientId()) |
| 323 | + .clientSecret(ctx.clientSecret()) |
| 324 | + .assertionCallback(assertionCtx -> { |
| 325 | + // RFC 8693 token exchange at the IdP: ID Token → ID-JAG |
| 326 | + DiscoverAndRequestJwtAuthGrantOptions jagOptions = DiscoverAndRequestJwtAuthGrantOptions |
| 327 | + .builder() |
| 328 | + .idpUrl(ctx.idpIssuer()) |
| 329 | + .idpTokenEndpoint(ctx.idpTokenEndpoint()) |
| 330 | + .idToken(ctx.idpIdToken()) |
| 331 | + .clientId(ctx.idpClientId()) |
| 332 | + .audience(assertionCtx.getAuthorizationServerUrl().toString()) |
| 333 | + .resource(assertionCtx.getResourceUrl().toString()) |
| 334 | + .build(); |
| 335 | + return EnterpriseAuth.discoverAndRequestJwtAuthorizationGrant(jagOptions, httpClient); |
| 336 | + }) |
| 337 | + .build(); |
| 338 | + |
| 339 | + EnterpriseAuthProvider provider = new EnterpriseAuthProvider(options, httpClient); |
| 340 | + |
| 341 | + HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(serverUrl) |
| 342 | + .httpRequestCustomizer(provider) |
| 343 | + .build(); |
| 344 | + |
| 345 | + McpSyncClient client = McpClient.sync(transport) |
| 346 | + .clientInfo(new McpSchema.Implementation("test-client", "1.0.0")) |
| 347 | + .requestTimeout(Duration.ofSeconds(30)) |
| 348 | + .build(); |
| 349 | + |
| 350 | + try { |
| 351 | + client.initialize(); |
| 352 | + System.out.println("Successfully connected to MCP server"); |
| 353 | + |
| 354 | + client.listTools(); |
| 355 | + System.out.println("Successfully listed tools"); |
| 356 | + } |
| 357 | + finally { |
| 358 | + client.close(); |
| 359 | + System.out.println("Connection closed successfully"); |
| 360 | + } |
| 361 | + } |
| 362 | + |
| 363 | + /** |
| 364 | + * Context provided by the conformance suite for the cross-app-access-complete-flow |
| 365 | + * scenario via the {@code MCP_CONFORMANCE_CONTEXT} environment variable. |
| 366 | + */ |
| 367 | + @JsonIgnoreProperties(ignoreUnknown = true) |
| 368 | + private record CrossAppAccessContext(@JsonProperty("client_id") String clientId, |
| 369 | + @JsonProperty("client_secret") String clientSecret, |
| 370 | + @JsonProperty("idp_client_id") String idpClientId, |
| 371 | + @JsonProperty("idp_id_token") String idpIdToken, @JsonProperty("idp_issuer") String idpIssuer, |
| 372 | + @JsonProperty("idp_token_endpoint") String idpTokenEndpoint) { |
| 373 | + } |
| 374 | + |
286 | 375 | } |
0 commit comments