feat: Add Enterprise Managed Authorization (SEP-990) support#1
feat: Add Enterprise Managed Authorization (SEP-990) support#1prachi-okta wants to merge 15 commits intomainfrom
Conversation
…ol#827) - Small javaformat fixes - Closes modelcontextprotocolgh-827 Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
- The server-servlet app depends on mcp, which must be either installed or included in the compile artifact for `mvn exec:java` to pick it up. - Change the build instructions to build from root, use a `-pl` flag to target the servlet app. - Disable the exec plugin from the root pom. Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
Clients can now subscribe to specific resources and receive targeted notifications when those resources change. Previously, calling `notifyResourceUpdated` on the server would broadcast the notification to every connected client regardless of interest — now only sessions that have explicitly subscribed to a given resource URI receive the update, making resource change propagation both correct and efficient. The subscription lifecycle is fully handled: clients can subscribe and unsubscribe at any time, and the server cleans up subscription state when a session closes. Supersedes modelcontextprotocol#838. Resolves modelcontextprotocol#837, modelcontextprotocol#776. --------- Signed-off-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
…extprotocol#826) Also add a test simulating a different locale in an isolated process. Resolves modelcontextprotocol#295
Signed-off-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
…odelcontextprotocol#843) Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
…ngle CPU (modelcontextprotocol#854) Signed-off-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
…contextprotocol#861) HttpClientStreamHttpTransport: add authorization error handler - Closes modelcontextprotocol#240 Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
…otocol#863) - Fix malformed SCM developerConnection URL (slash → colon) across all modules - Add mcp-json-jackson3 to mcp-bom dependency management - Update license URL to HTTPS - Fix POM's scm definitions Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com> Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com> --------- Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
| public static Mono<String> requestJwtAuthorizationGrant(RequestJwtAuthGrantOptions options, HttpClient httpClient) { | ||
| return Mono.defer(() -> { | ||
| List<String> params = new ArrayList<>(); | ||
| params.add(encode("grant_type") + "=" + encode(GRANT_TYPE_TOKEN_EXCHANGE)); |
There was a problem hiding this comment.
nit: We can have a utility method here to encode and append with "=" as well
There was a problem hiding this comment.
Done. added a private encodeParam(String key, String value) helper that returns encode(key) + "=" + encode(value). Both requestJwtAuthorizationGrant and exchangeJwtBearerGrant now use it.
| HttpClient httpClient) { | ||
| return Mono.defer(() -> { | ||
| List<String> params = new ArrayList<>(); | ||
| params.add(encode("grant_type") + "=" + encode(GRANT_TYPE_JWT_BEARER)); |
There was a problem hiding this comment.
nit: We can have a utility method here to encode and with "=" append as well
There was a problem hiding this comment.
Done. added a private encodeParam(String key, String value) helper that returns encode(key) + "=" + encode(value). Both requestJwtAuthorizationGrant and exchangeJwtBearerGrant now use it.
| * | ||
| * @author MCP SDK Contributors | ||
| */ | ||
| public class DiscoverAndRequestJwtAuthGrantOptions { |
There was a problem hiding this comment.
Can it not just extend RequestJwtAuthGrantOptions ?
There was a problem hiding this comment.
Done. DiscoverAndRequestJwtAuthGrantOptions now extends RequestJwtAuthGrantOptions. The duplicated fields were removed and the nested Builder extends RequestJwtAuthGrantOptions.Builder, with idpTokenEndpoint mapped directly to the inherited tokenEndpoint.
| * Proactive refresh buffer: treat a token as expired this many seconds before its | ||
| * actual expiry to avoid using a token that expires mid-flight. | ||
| */ | ||
| private static final Duration EXPIRY_BUFFER = Duration.ofSeconds(30); |
There was a problem hiding this comment.
Can we rename EXPIRY_BUFFER to EXPIRY_BUFFER_IN_SEC or something ?
There was a problem hiding this comment.
Added the TOKEN_ prefix instead for consistency with the other constants TOKEN_EXPIRY_BUFFER.
| if (!TOKEN_TYPE_ID_JAG.equalsIgnoreCase(tokenResponse.getIssuedTokenType())) { | ||
| return Mono.error(new EnterpriseAuthException("Unexpected issued_token_type in JAG response: " | ||
| + tokenResponse.getIssuedTokenType() + " (expected " + TOKEN_TYPE_ID_JAG + ")")); | ||
| } | ||
| if (!"N_A".equalsIgnoreCase(tokenResponse.getTokenType())) { | ||
| return Mono.error(new EnterpriseAuthException("Unexpected token_type in JAG response: " | ||
| + tokenResponse.getTokenType() + " (expected N_A per RFC 8693 §2.2.1)")); | ||
| } | ||
| if (tokenResponse.getAccessToken() == null || tokenResponse.getAccessToken().isBlank()) { | ||
| return Mono | ||
| .error(new EnterpriseAuthException("JAG token exchange response is missing access_token")); | ||
| } |
There was a problem hiding this comment.
nit: We may extract to validateJAGTokenExchangeResponse
There was a problem hiding this comment.
Done. the validation block is extracted into a private validateJAGTokenExchangeResponse(JagTokenExchangeResponse) method.
| * DiscoverAndRequestJwtAuthGrantOptions.builder() | ||
| * .idpUrl(ctx.getAuthorizationServerUrl().toString()) | ||
| * .idToken(myIdTokenSupplier.get()) | ||
| * .clientId("idp-client-id") |
There was a problem hiding this comment.
Don't we need to pass client_secret here for ID_JAG exchange ?
There was a problem hiding this comment.
updated to include .clientSecret("idp-client-secret")
| if (options.getIdpTokenEndpoint() != null) { | ||
| tokenEndpointMono = Mono.just(options.getIdpTokenEndpoint()); | ||
| } |
There was a problem hiding this comment.
Is this to handle a case where IDP authorization server metadata is already discovered in a separate step ?
There was a problem hiding this comment.
Yes so if the caller has already performed RFC 8414 discovery (or simply knows the IdP token endpoint ahead of time), they can set idpTokenEndpoint to skip the discovery round-trip
| return options.getAssertionCallback().apply(assertionContext).flatMap(assertion -> { | ||
| ExchangeJwtBearerGrantOptions exchangeOptions = ExchangeJwtBearerGrantOptions.builder() | ||
| .tokenEndpoint(metadata.getTokenEndpoint()) | ||
| .assertion(assertion) |
There was a problem hiding this comment.
Can it happen that the Access Token from the resource server is expired , but the ID-JAG from IDP for the resource server is still valid and the same can be reused while exchanging access token from a resource server ?
There was a problem hiding this comment.
Yes, it can
The ID-JAG may outlive the access token depending on the IdP's configuration. EnterpriseAuthProvider intentionally does not cache the JAG itself - each access token refresh calls the assertionCallback to get a fresh JAG. If callers want to avoid the extra IdP round-trip, they can implement JAG caching inside their own assertionCallback
| * @see EnterpriseAuth | ||
| * @see EnterpriseAuthProviderOptions | ||
| */ | ||
| public class EnterpriseAuthProvider implements McpAsyncHttpClientRequestCustomizer { |
There was a problem hiding this comment.
Can we write unit tests for this as well ?
| JwtBearerAccessTokenResponse tokenResponse = mapper.readValue(response.body(), | ||
| JwtBearerAccessTokenResponse.class); | ||
|
|
||
| if (tokenResponse.getAccessToken() == null || tokenResponse.getAccessToken().isBlank()) { |
There was a problem hiding this comment.
Can we assume that the resource authorization server will never return a refresh token here ? And if it happens , does in increase security risk ?
There was a problem hiding this comment.
RFC 7523 is a stateless grant so no refresh token is involved by design, so we don't expect one. If the AS returns one anyway, we ignore it, since using it would let the client skip re-validating the user's identity with the IdP and bypass session/revocation policies. Added a code comment for this.
mcp-core/src/test/java/io/modelcontextprotocol/client/auth/EnterpriseAuthTest.java
Outdated
Show resolved
Hide resolved
26a6fc6 to
649a426
Compare
Address PR review comments for Enterprise Managed Authorization (SEP-990)
Changes
EnterpriseAuth.java
encodeParam(key, value)utility method to replace inlineencode(k) + "=" + encode(v)callsvalidateJAGTokenExchangeResponse()method for JAG response validationtoken_typevalidation: removed strict"N_A"check per RFC 8693 §2.2.1 —token_typeis informational when the issued token is not an access token; strict checking rejects conformant IdPsexchangeJwtBearerGranttoclient_secret_basic: credentials now sent viaAuthorization: Basicheader instead of request body, aligning with TypeScript SDK and SEP-990 conformance requirementsrefresh_tokenis intentionally ignored (RFC 7523 is stateless by design; using a refresh token would bypass IdP session/revocation policies)idpTokenEndpointshortcut (skips RFC 8414 discovery when endpoint is already known)DiscoverAndRequestJwtAuthGrantOptions.java
RequestJwtAuthGrantOptions, eliminating duplicated fieldsEnterpriseAuthProvider.java
EXPIRY_BUFFER→TOKEN_EXPIRY_BUFFERfor consistencyassertionCallbackto reduce IdP round-tripsExchangeJwtBearerGrantOptions.java / JagTokenExchangeResponse.java
client_secret_basicauth method and relaxedtoken_typesemanticsEnterpriseAuthTest.java
EnterpriseAuthProvider: header injection, token caching, cache invalidation, proactive expiry refresh, tokens withoutexpires_in, discovery failure, assertion callback errorsexchangeJwtBearerGranttest to assertAuthorization: BasicheaderwrongTokenType_emitsErrortest withnonStandardTokenType_succeedsto verify leniency