Support SEP-990 Cross-App Access via ID-JAG and the jwt-bearer Grant#454
Open
koic wants to merge 1 commit into
Open
Support SEP-990 Cross-App Access via ID-JAG and the jwt-bearer Grant#454koic wants to merge 1 commit into
koic wants to merge 1 commit into
Conversation
## Motivation and Context The MCP Enterprise Managed Authorization extension (SEP-990, modelcontextprotocol/modelcontextprotocol#990, accepted/final, published in the ext-auth repository) lets enterprise identity providers govern MCP authorization: the client exchanges an IdP-issued ID token for an Identity Assertion Authorization Grant (ID-JAG) via RFC 8693 token exchange at the IdP, then presents the ID-JAG to the MCP authorization server with the RFC 7523 `jwt-bearer` grant, authenticating with `client_secret_basic`. The `auth/cross-app-access-complete-flow` conformance scenario exercises the complete flow and previously failed its two core checks (the Ruby client ran the authorization-code flow instead). This mirrors the merged TypeScript SDK implementation (`requestJwtAuthorizationGrant` / `CrossAppAccessProvider` in typescript-sdk; the Python SDK tracks the same work in the open python-sdk#1721), adapted to this SDK's provider design: - New `MCP::Client::OAuth::IDJAGTokenExchange.request` performs the RFC 8693 exchange at the IdP token endpoint (`subject_token_type` id_token, `requested_token_type` id-jag, `audience` = the MCP authorization server's issuer, `resource` = the canonical MCP server URL) and validates that the response's `issued_token_type` is an ID-JAG before returning the assertion. The ID-JAG itself is treated as opaque, as in the TypeScript SDK. - New `MCP::Client::OAuth::CrossAppAccessProvider` declares `authorization_flow :jwt_bearer` and takes the assertion from a callable (`call(audience:, resource:)`), so the common `IDJAGTokenExchange` case, enterprise secret stores, and tests all plug in the same way (the TypeScript provider's assertion callback works identically). The MCP AS credentials are stored as `client_secret_basic` client information. - `Flow` dispatches `:jwt_bearer` alongside `:client_credentials`: discovery, issuer validation, and endpoint security checks are shared with `run!`, then `run_jwt_bearer!` requests the assertion with the validated issuer as the audience and posts the `jwt-bearer` grant through the existing `post_to_token_endpoint`. - The conformance client builds a `CrossAppAccessProvider` when the harness injects `idp_id_token` (exchanging it at `idp_token_endpoint` with `idp_client_id`, like the TypeScript and Python conformance clients), and `auth/cross-app-access-complete-flow` is removed from `conformance/expected_failures.yml`. ## How Has This Been Tested? - New `test/mcp/client/oauth/id_jag_token_exchange_test.rb` asserts the exact RFC 8693 form parameters on the wire and the response validation: a non-ID-JAG `issued_token_type` is rejected, as are a missing `access_token`, non-2xx responses, unparseable bodies, and non-object JSON. - New `test/mcp/client/oauth/cross_app_access_provider_test.rb` covers the provider surface: stored `client_secret_basic` credentials, the `:jwt_bearer` flow declaration, audience/resource passthrough to the assertion callable, constructor validation (missing client_id, missing client_secret, non-callable assertion provider), and storage delegation. - New tests in `test/mcp/client/oauth/flow_test.rb` run the full grant against WebMock stubs: the token request carries `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`, the assertion, the RFC 8707 `resource`, and HTTP Basic credentials, with no DCR and no authorization-code machinery; the assertion callable receives the validated issuer as `audience` and the canonical server URL as `resource`; an empty assertion aborts before the token endpoint is contacted. ## Breaking Changes None. `IDJAGTokenExchange` and `CrossAppAccessProvider` are new, and the `Flow` dispatch only activates for providers that declare `authorization_flow :jwt_bearer`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation and Context
The MCP Enterprise Managed Authorization extension (SEP-990, modelcontextprotocol/modelcontextprotocol#990, accepted/final, published in the ext-auth repository) lets enterprise identity providers govern MCP authorization: the client exchanges an IdP-issued ID token for an Identity Assertion Authorization Grant (ID-JAG) via RFC 8693 token exchange at the IdP, then presents the ID-JAG to the MCP authorization server with the RFC 7523
jwt-bearergrant, authenticating withclient_secret_basic. Theauth/cross-app-access-complete-flowconformance scenario exercises the complete flow and previously failed its two core checks (the Ruby client ran the authorization-code flow instead).This mirrors the merged TypeScript SDK implementation (
requestJwtAuthorizationGrant/CrossAppAccessProviderin typescript-sdk; the Python SDK tracks the same work in the open python-sdk#1721), adapted to this SDK's provider design:MCP::Client::OAuth::IDJAGTokenExchange.requestperforms the RFC 8693 exchange at the IdP token endpoint (subject_token_typeid_token,requested_token_typeid-jag,audience= the MCP authorization server's issuer,resource= the canonical MCP server URL) and validates that the response'sissued_token_typeis an ID-JAG before returning the assertion. The ID-JAG itself is treated as opaque, as in the TypeScript SDK.MCP::Client::OAuth::CrossAppAccessProviderdeclaresauthorization_flow :jwt_bearerand takes the assertion from a callable (call(audience:, resource:)), so the commonIDJAGTokenExchangecase, enterprise secret stores, and tests all plug in the same way (the TypeScript provider's assertion callback works identically). The MCP AS credentials are stored asclient_secret_basicclient information.Flowdispatches:jwt_beareralongside:client_credentials: discovery, issuer validation, and endpoint security checks are shared withrun!, thenrun_jwt_bearer!requests the assertion with the validated issuer as the audience and posts thejwt-bearergrant through the existingpost_to_token_endpoint.CrossAppAccessProviderwhen the harness injectsidp_id_token(exchanging it atidp_token_endpointwithidp_client_id, like the TypeScript and Python conformance clients), andauth/cross-app-access-complete-flowis removed fromconformance/expected_failures.yml.How Has This Been Tested?
test/mcp/client/oauth/id_jag_token_exchange_test.rbasserts the exact RFC 8693 form parameters on the wire and the response validation: a non-ID-JAGissued_token_typeis rejected, as are a missingaccess_token, non-2xx responses, unparseable bodies, and non-object JSON.test/mcp/client/oauth/cross_app_access_provider_test.rbcovers the provider surface: storedclient_secret_basiccredentials, the:jwt_bearerflow declaration, audience/resource passthrough to the assertion callable, constructor validation (missing client_id, missing client_secret, non-callable assertion provider), and storage delegation.test/mcp/client/oauth/flow_test.rbrun the full grant against WebMock stubs: the token request carriesgrant_type=urn:ietf:params:oauth:grant-type:jwt-bearer, the assertion, the RFC 8707resource, and HTTP Basic credentials, with no DCR and no authorization-code machinery; the assertion callable receives the validated issuer asaudienceand the canonical server URL asresource; an empty assertion aborts before the token endpoint is contacted.Breaking Changes
None.
IDJAGTokenExchangeandCrossAppAccessProviderare new, and theFlowdispatch only activates for providers that declareauthorization_flow :jwt_bearer.Types of changes
Checklist