feat: add conformance tests for SEP-990#110
Conversation
There was a problem hiding this comment.
A few high level comments:
- The top of the PR description seems outdated / copy pasted? (the SSE polling bit)
- It looks like we're mixing AS and IdP endpoints, let's keep those separate w/ separate handlers
- let's stick to 1 end-to-end test to start w/ many checks. each test that spins up a server is a cost on CI for every SDK, so we want to keep the # low.
- I stuck with comments on the test since that's the most important part, but the example will also need some changes.
- if you could include a negative test (i.e. an example client that implements it incorrectly, and so it will fail the test), that'd be great.
- please add this to the "extensions" list of tests (may need to manage merge conflicts, this jostled a bit for the tiering kickoff)
| 'urn:ietf:params:oauth:grant-type:token-exchange', | ||
| 'urn:ietf:params:oauth:grant-type:jwt-bearer' | ||
| ], | ||
| tokenEndpointAuthMethodsSupported: ['none'], |
There was a problem hiding this comment.
i believe this should be client_secret_basic or private_key_jwt since ID-JAG is only supposed to work with confidential clients.
There was a problem hiding this comment.
Added the tokenEndpointAuthMethodsSupported to mentioned parameters. However currently client_secret_basic is considered and private_key_jwt would be checked upon later.
| const idpIdToken = await createIdpIdToken( | ||
| this.idpPrivateKey!, | ||
| this.idpServer.getUrl(), | ||
| this.authServer.getUrl() |
There was a problem hiding this comment.
the IdP ID token should not have the auth server as its audience. it should be the "idp_client_id" which is distinct from the client id used to talk to the authorization server.
it's the id-jag that should have the authServer as its audience.
There was a problem hiding this comment.
Thanks for pointing this out, since ID token would generally be provided by user kept this as a bypass value initially but now it is been corrected.
| tokenEndpointAuthMethodsSupported: ['none'], | ||
| onTokenRequest: async ({ grantType, body, timestamp, authBaseUrl }) => { | ||
| // Handle token exchange (IDP ID token -> authorization grant) | ||
| if (grantType === 'urn:ietf:params:oauth:grant-type:token-exchange') { |
There was a problem hiding this comment.
the auth server shouldn't bet getting a token-exchange request, that request should be going to the IdP.
There was a problem hiding this comment.
Separated the ID-JAG and access token exchange steps.
| scopes: [], | ||
| additionalFields: { | ||
| issued_token_type: | ||
| 'urn:ietf:params:oauth:token-type:authorization_grant', |
There was a problem hiding this comment.
This is the ID-JAG flow, so should be urn:ietf:params:oauth:token-type:id-jag
| * using RFC 8693 token exchange, and then exchange that grant for an access token | ||
| * using RFC 7523 JWT Bearer grant. | ||
| */ | ||
| export class CrossAppAccessTokenExchangeScenario implements Scenario { |
There was a problem hiding this comment.
let's start with just 1 test for the full flow. each test adds integration cost, and these 2 are redundant with the full e2e test.
There was a problem hiding this comment.
Done. Removed two separate flow check methods and kept only one full flow step.
| // Start auth server with both token exchange and JWT bearer grant support | ||
| const authApp = createAuthServer(this.checks, this.authServer.getUrl, { | ||
| grantTypesSupported: [ | ||
| 'urn:ietf:params:oauth:grant-type:token-exchange', |
There was a problem hiding this comment.
the auth server doesn't need to support token-exchange, I think you've combined the IdP and AS in this test.
There was a problem hiding this comment.
Separated the ID-JAG and access token exchange steps.
|
|
||
| if ( | ||
| !subjectToken || | ||
| subjectTokenType !== 'urn:ietf:params:oauth:token-type:id_token' |
There was a problem hiding this comment.
the id_token should never be hitting the AS url, this function is called in the AS.
There was a problem hiding this comment.
Separated the ID-JAG and access token exchange steps.
| sub: userId, | ||
| grant_type: 'authorization_grant' | ||
| }) | ||
| .setProtectedHeader({ alg: 'ES256' }) |
There was a problem hiding this comment.
.setProtectedHeader({ alg: 'ES256', typ: 'oauth-id-jag+jwt' })
| return { | ||
| token: authorizationGrant, | ||
| scopes: [], | ||
| additionalFields: { |
There was a problem hiding this comment.
i don't think this field is respected. since this handler needs to be on the IdP anyway, it's probably better to implement a handler on the fake IdP directly rather than try to shoehorn into the createAuthServer interfaces
commit: |
6c0de83 to
ec2e5ab
Compare
|
Hi @pcarleton , Thanks for the review. I've made the changes as required and rebased with latest main, please have a look at the new changes once you are available. |
|
|
||
| // Step 1: Token Exchange (IDP ID token -> ID-JAG) | ||
| logger.debug('Step 1: Exchanging IDP ID token for ID-JAG at IdP...'); | ||
| const tokenExchangeParams = new URLSearchParams({ |
There was a problem hiding this comment.
| Parameter | Required/Optional | Description | Example/Allowed Values |
|---|---|---|---|
| requested_token_type | REQUIRED | Indicates that an ID Assertion JWT is being requested. | urn:ietf:params:oauth:token-type:id-jag |
| audience | REQUIRED | The Issuer URL of the MCP server's authorization server. | https://auth.chat.example/ |
| resource | REQUIRED | The RFC9728 Resource Identifier of the MCP server. | https://mcp.chat.example/ |
| scope | OPTIONAL | The space-separated list of scopes at the MCP Server that are being requested. | scope1 scope2 |
| subject_token | REQUIRED | The identity assertion (e.g. the OpenID Connect ID Token or SAML assertion) for the target end-user. | (JWT or SAML assertion string) |
| subject_token_type | REQUIRED | Indicates the type of the security token in the subject_token parameter, as specified in RFC8693 Section 3. | urn:ietf:params:oauth:token-type:id_token (OIDC)urn:ietf:params:oauth:token-type:saml2 (SAML) |
we're missing several required parameters here.
| const jwtBearerParams = new URLSearchParams({ | ||
| grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', | ||
| assertion: idJag, | ||
| client_id: ctx.client_id |
There was a problem hiding this comment.
this needs to be a distinct client id from the one used for the IdP
| grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', | ||
| subject_token: ctx.idp_id_token, | ||
| subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', | ||
| client_id: ctx.client_id |
There was a problem hiding this comment.
| client_id: ctx.client_id | |
| client_id: ctx.idp_client_id |
| idp_id_token: idpIdToken, | ||
| idp_issuer: this.idpServer.getUrl(), | ||
| idp_token_endpoint: `${this.idpServer.getUrl()}/token`, | ||
| auth_server_url: this.authServer.getUrl() |
There was a problem hiding this comment.
because we need resource as well, I think it's better to get this via discovery (i.e. not provide via context)
| } | ||
| ); | ||
|
|
||
| this.checks.push({ |
There was a problem hiding this comment.
we should verify the full set of required params for the token exchange
There was a problem hiding this comment.
these other scenarios are unused, please delete
- Delete unused separate token-exchange and jwt-bearer scenarios, keeping only the complete e2e flow (review comment) - Add missing required token exchange params per SEP-990 spec: requested_token_type, audience, resource (review comment) - Use ctx.idp_client_id for token exchange client_id instead of AS client_id (review comment) - Client discovers resource and auth server via PRM metadata instead of receiving auth_server_url via context (review comment) - Server IdP handler verifies all required token exchange params with detailed error messages (review comment) - Add resource, client_id, jti claims to ID-JAG per SEP-990 spec - Verify ID-JAG typ header (oauth-id-jag+jwt) in JWT bearer handler - Remove auth_server_url from context schema
Server-side (AS) now verifies: - client_secret_basic authentication on JWT bearer grant - ID-JAG typ header is oauth-id-jag+jwt - ID-JAG client_id claim matches the authenticating client (Section 5.1) - ID-JAG resource claim matches the MCP server resource identifier - Client credentials provided via context (client_secret) Server-side (IdP) now: - Sets ID-JAG client_id to the MCP Client's AS client_id (not the IdP client_id), per Section 6.1 Example client now: - Authenticates to AS via client_secret_basic (Authorization: Basic) instead of sending client_id in body - Checks AS metadata grant_types_supported includes jwt-bearer before attempting the flow
- Add shared MockTokenVerifier between AS and MCP server so the MCP server only accepts tokens actually issued by the auth server, matching the pattern used by all other auth scenarios - Remove private_key_jwt from tokenEndpointAuthMethodsSupported since the handler only implements client_secret_basic
…tprotocol/conformance v0.1.14 Refactored enterprise managed authorization implementation and conformance tests to use the official @modelcontextprotocol/conformance npm package (v0.1.14) instead of custom mock servers. **Key Changes:** **Implementation:** - Refactored `EnterpriseAuthOAuthClientProvider._perform_authorization()` to return `httpx.Request` instead of executing requests directly - Moved error handling to parent class `OAuthClientProvider.async_auth_flow()` - Updated method signatures: - `exchange_token_for_id_jag(client)` → returns `str` (ID-JAG) - `exchange_id_jag_for_access_token(id_jag)` → returns `httpx.Request` (not OAuthToken) **Conformance Tests:** - Migrated from custom mock server to official conformance package v0.1.14 - Removed custom `enterprise_auth_server.py` (332 lines) - Removed custom `run-enterprise-auth-with-server.sh` (169 lines) - Added `run-enterprise-auth-conformance.sh` using official conformance scenarios - Updated `client.py` to support SEP-990 conformance tests: - `auth/cross-app-access-complete-flow` - `auth/enterprise-token-exchange` - `auth/enterprise-jwt-bearer` - All 9/9 conformance checks passing **Tests:** - Added 2 new test cases for 100% code coverage - Updated 32 existing tests to match new implementation - Removed 3 duplicate/skipped tests - Total: 29 tests passing, 0 failing, 100% coverage **Documentation:** - Updated `README.md` Enterprise Managed Authorization section - Replaced manual token exchange examples with automatic auth flow pattern - Added advanced manual flow example showing correct method signatures - Updated `examples/snippets/clients/enterprise_managed_auth_client.py` - Removed unused imports **Related:** - PR: modelcontextprotocol/conformance#110 - Spec: SEP-990 (Enterprise Managed Authorization) - Package: @modelcontextprotocol/conformance@0.1.14 **Testing:** - ✅ All unit tests passing - ✅ 100% code coverage for enterprise_managed_auth.py - ✅ Conformance tests passing (9/9 checks) - ✅ GitHub Actions workflow updated
…tprotocol/conformance v0.1.14 Refactored enterprise managed authorization implementation and conformance tests to use the official @modelcontextprotocol/conformance npm package (v0.1.14) instead of custom mock servers. **Key Changes:** **Implementation:** - Refactored `EnterpriseAuthOAuthClientProvider._perform_authorization()` to return `httpx.Request` instead of executing requests directly - Moved error handling to parent class `OAuthClientProvider.async_auth_flow()` - Updated method signatures: - `exchange_token_for_id_jag(client)` → returns `str` (ID-JAG) - `exchange_id_jag_for_access_token(id_jag)` → returns `httpx.Request` (not OAuthToken) **Conformance Tests:** - Migrated from custom mock server to official conformance package v0.1.14 - Removed custom `enterprise_auth_server.py` (332 lines) - Removed custom `run-enterprise-auth-with-server.sh` (169 lines) - Added `run-enterprise-auth-conformance.sh` using official conformance scenarios - Updated `client.py` to support SEP-990 conformance tests: - `auth/cross-app-access-complete-flow` - `auth/enterprise-token-exchange` - `auth/enterprise-jwt-bearer` - All 9/9 conformance checks passing **Tests:** - Added 2 new test cases for 100% code coverage - Updated 32 existing tests to match new implementation - Removed 3 duplicate/skipped tests - Total: 29 tests passing, 0 failing, 100% coverage **Documentation:** - Updated `README.md` Enterprise Managed Authorization section - Replaced manual token exchange examples with automatic auth flow pattern - Added advanced manual flow example showing correct method signatures - Updated `examples/snippets/clients/enterprise_managed_auth_client.py` - Removed unused imports **Related:** - PR: modelcontextprotocol/conformance#110 - Spec: SEP-990 (Enterprise Managed Authorization) - Package: @modelcontextprotocol/conformance@0.1.14 **Testing:** - ✅ All unit tests passing - ✅ 100% code coverage for enterprise_managed_auth.py - ✅ Conformance tests passing (9/9 checks) - ✅ GitHub Actions workflow updated
…tprotocol/conformance v0.1.14 Refactored enterprise managed authorization implementation and conformance tests to use the official @modelcontextprotocol/conformance npm package (v0.1.14) instead of custom mock servers. **Key Changes:** **Implementation:** - Refactored `EnterpriseAuthOAuthClientProvider._perform_authorization()` to return `httpx.Request` instead of executing requests directly - Moved error handling to parent class `OAuthClientProvider.async_auth_flow()` - Updated method signatures: - `exchange_token_for_id_jag(client)` → returns `str` (ID-JAG) - `exchange_id_jag_for_access_token(id_jag)` → returns `httpx.Request` (not OAuthToken) **Conformance Tests:** - Migrated from custom mock server to official conformance package v0.1.14 - Removed custom `enterprise_auth_server.py` (332 lines) - Removed custom `run-enterprise-auth-with-server.sh` (169 lines) - Added `run-enterprise-auth-conformance.sh` using official conformance scenarios - Updated `client.py` to support SEP-990 conformance tests: - `auth/cross-app-access-complete-flow` - `auth/enterprise-token-exchange` - `auth/enterprise-jwt-bearer` - All 9/9 conformance checks passing **Tests:** - Added 2 new test cases for 100% code coverage - Updated 32 existing tests to match new implementation - Removed 3 duplicate/skipped tests - Total: 29 tests passing, 0 failing, 100% coverage **Documentation:** - Updated `README.md` Enterprise Managed Authorization section - Replaced manual token exchange examples with automatic auth flow pattern - Added advanced manual flow example showing correct method signatures - Updated `examples/snippets/clients/enterprise_managed_auth_client.py` - Removed unused imports **Related:** - PR: modelcontextprotocol/conformance#110 - Spec: SEP-990 (Enterprise Managed Authorization) - Package: @modelcontextprotocol/conformance@0.1.14 **Testing:** - ✅ All unit tests passing - ✅ 100% code coverage for enterprise_managed_auth.py - ✅ Conformance tests passing (9/9 checks) - ✅ GitHub Actions workflow updated
…tprotocol/conformance v0.1.14 Refactored enterprise managed authorization implementation and conformance tests to use the official @modelcontextprotocol/conformance npm package (v0.1.14) instead of custom mock servers. **Key Changes:** **Implementation:** - Refactored `EnterpriseAuthOAuthClientProvider._perform_authorization()` to return `httpx.Request` instead of executing requests directly - Moved error handling to parent class `OAuthClientProvider.async_auth_flow()` - Updated method signatures: - `exchange_token_for_id_jag(client)` → returns `str` (ID-JAG) - `exchange_id_jag_for_access_token(id_jag)` → returns `httpx.Request` (not OAuthToken) **Conformance Tests:** - Migrated from custom mock server to official conformance package v0.1.14 - Removed custom `enterprise_auth_server.py` (332 lines) - Removed custom `run-enterprise-auth-with-server.sh` (169 lines) - Added `run-enterprise-auth-conformance.sh` using official conformance scenarios - Updated `client.py` to support SEP-990 conformance tests: - `auth/cross-app-access-complete-flow` - `auth/enterprise-token-exchange` - `auth/enterprise-jwt-bearer` - All 9/9 conformance checks passing **Tests:** - Added 2 new test cases for 100% code coverage - Updated 32 existing tests to match new implementation - Removed 3 duplicate/skipped tests - Total: 29 tests passing, 0 failing, 100% coverage **Documentation:** - Updated `README.md` Enterprise Managed Authorization section - Replaced manual token exchange examples with automatic auth flow pattern - Added advanced manual flow example showing correct method signatures - Updated `examples/snippets/clients/enterprise_managed_auth_client.py` - Removed unused imports **Related:** - PR: modelcontextprotocol/conformance#110 - Spec: SEP-990 (Enterprise Managed Authorization) - Package: @modelcontextprotocol/conformance@0.1.14 **Testing:** - ✅ All unit tests passing - ✅ 100% code coverage for enterprise_managed_auth.py - ✅ Conformance tests passing (9/9 checks) - ✅ GitHub Actions workflow updated

Description:
Adds conformance tests for SEP-990 which introduces Enterprise Managed OAuth for machine-to-machine authentication using enterprise identity providers without user interaction.
Summary
oauth-id-jag+jwt), audience claims, and confidential client authenticationMotivation and Context
SEP-990 introduces Enterprise Managed OAuth, enabling machine-to-machine authentication flows for cross-app access in enterprise environments. This allows applications to authenticate using enterprise identity providers (IdPs) through a two-step OAuth flow:
These conformance tests ensure SDK implementations correctly handle the complete cross-app access flow including proper endpoint separation, token type validation, and audience claim verification.
How Has This Been Tested?
Breaking Changes
None.
Types of changes
Checklist
References