Skip to content

feat: Add Enterprise Managed Authorization (SEP-990) support#1

Open
prachi-okta wants to merge 15 commits intomainfrom
feature/enterprise-managed-authorization
Open

feat: Add Enterprise Managed Authorization (SEP-990) support#1
prachi-okta wants to merge 15 commits intomainfrom
feature/enterprise-managed-authorization

Conversation

@prachi-okta
Copy link
Owner

@prachi-okta prachi-okta commented Mar 2, 2026

Address PR review comments for Enterprise Managed Authorization (SEP-990)

Changes

EnterpriseAuth.java

  • Added encodeParam(key, value) utility method to replace inline encode(k) + "=" + encode(v) calls
  • Extracted validateJAGTokenExchangeResponse() method for JAG response validation
  • Relaxed token_type validation: removed strict "N_A" check per RFC 8693 §2.2.1 — token_type is informational when the issued token is not an access token; strict checking rejects conformant IdPs
  • Switched exchangeJwtBearerGrant to client_secret_basic: credentials now sent via Authorization: Basic header instead of request body, aligning with TypeScript SDK and SEP-990 conformance requirements
  • Added code comment explaining why refresh_token is intentionally ignored (RFC 7523 is stateless by design; using a refresh token would bypass IdP session/revocation policies)
  • Added code comment explaining the idpTokenEndpoint shortcut (skips RFC 8414 discovery when endpoint is already known)

DiscoverAndRequestJwtAuthGrantOptions.java

  • Now extends RequestJwtAuthGrantOptions, eliminating duplicated fields

EnterpriseAuthProvider.java

  • Renamed EXPIRY_BUFFERTOKEN_EXPIRY_BUFFER for consistency
  • Added code comment explaining that the ID-JAG is not cached by the provider; callers can cache it in their assertionCallback to reduce IdP round-trips

ExchangeJwtBearerGrantOptions.java / JagTokenExchangeResponse.java

  • Updated Javadoc to reflect client_secret_basic auth method and relaxed token_type semantics

EnterpriseAuthTest.java

  • Added full unit test coverage for EnterpriseAuthProvider: header injection, token caching, cache invalidation, proactive expiry refresh, tokens without expires_in, discovery failure, assertion callback errors
  • Updated exchangeJwtBearerGrant test to assert Authorization: Basic header
  • Replaced wrongTokenType_emitsError test with nonStandardTokenType_succeeds to verify leniency

Kehrlann and others added 11 commits March 2, 2026 11:33
…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));

Choose a reason for hiding this comment

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

nit: We can have a utility method here to encode and append with "=" as well

Copy link
Owner Author

Choose a reason for hiding this comment

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

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));

Choose a reason for hiding this comment

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

nit: We can have a utility method here to encode and with "=" append as well

Copy link
Owner Author

Choose a reason for hiding this comment

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

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 {

Choose a reason for hiding this comment

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

Can it not just extend RequestJwtAuthGrantOptions ?

Copy link
Owner Author

Choose a reason for hiding this comment

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

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);

Choose a reason for hiding this comment

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

Can we rename EXPIRY_BUFFER to EXPIRY_BUFFER_IN_SEC or something ?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Added the TOKEN_ prefix instead for consistency with the other constants TOKEN_EXPIRY_BUFFER.

Comment on lines +198 to +209
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"));
}

Choose a reason for hiding this comment

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

nit: We may extract to validateJAGTokenExchangeResponse

Copy link
Owner Author

Choose a reason for hiding this comment

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

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")

Choose a reason for hiding this comment

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

Don't we need to pass client_secret here for ID_JAG exchange ?

Copy link
Owner Author

Choose a reason for hiding this comment

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

updated to include .clientSecret("idp-client-secret")

Comment on lines +237 to +239
if (options.getIdpTokenEndpoint() != null) {
tokenEndpointMono = Mono.just(options.getIdpTokenEndpoint());
}

Choose a reason for hiding this comment

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

Is this to handle a case where IDP authorization server metadata is already discovered in a separate step ?

Copy link
Owner Author

Choose a reason for hiding this comment

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

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)

Choose a reason for hiding this comment

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

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 ?

Copy link
Owner Author

Choose a reason for hiding this comment

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

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 {

Choose a reason for hiding this comment

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

Can we write unit tests for this as well ?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done

JwtBearerAccessTokenResponse tokenResponse = mapper.readValue(response.body(),
JwtBearerAccessTokenResponse.class);

if (tokenResponse.getAccessToken() == null || tokenResponse.getAccessToken().isBlank()) {

Choose a reason for hiding this comment

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

Can we assume that the resource authorization server will never return a refresh token here ? And if it happens , does in increase security risk ?

Copy link
Owner Author

Choose a reason for hiding this comment

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

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.

Copy link

@prasenjit-karmakar prasenjit-karmakar left a comment

Choose a reason for hiding this comment

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

LGTM, One Minor comment

@prachi-okta prachi-okta force-pushed the feature/enterprise-managed-authorization branch from 26a6fc6 to 649a426 Compare March 16, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants