You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There is no conformance test for IdP AS and Resource AS of Enterprise-Managed Authorization. The issue is for adding conformance test for Resource AS of EMA
Strategy
Step-by-Step implementation
The conformance test will be implemented in step-by-step.
Firstly, lay the groundwork for implementing this test and implement some positive and negative tests based on that.
Next, implement all tests covering EMA's requirements for Resource AS.
The issue for the first work.
Scope
The issue targets Resource AS. IdP AS will be covered by other issue.
The issue targets testing the requirements of the specification with the label MUST, SHALL, REQUIRED, RECOMMENDED
Implement some positive and negative tests, not cover all EMA's requirements. The comprehensive testing will be covered by other issue.
The MCP Server that the Resource AS does not trust
Approach
The test runs in localhost.
The test runner simulates MCP Client.
The test runner prepares and controlls two entities: Trusted IdP AS and Untrusted IdP AS.
Trusted IdP AS and Untrusted IdP AS can provide the test runner with ID-JAG as it wants (not only valid one but also invalid one).
The test runner provide two endpoint of Trusted IdP AS: sever metadata endpoint (well-known URI) and JWK Set Endpoint (jwks_uri).
The server metadata endpoint provides a target Resource AS with jwks_uri.
The JWK Set Endpoint provides a target Resource AS with the key for verifying signature of ID-JAG.
On running a test scenario, the test runner as MCP Client sends JWT authorization grant with ID-JAG to the target Resource AS.
When receiving the response from the target Resource AS, the test runner judges if the test scenario successfully completes or not.
sequenceDiagram
autonumber
participant Client as MCP Client<br/>(test runner)
participant RAS as Resource AS<br/>(test target)
participant IdP as Trusted IdP AS<br/>(test runner)
Note over Client,IdP: Create ID-JAG
Note over Client: Resource AS Issuer URL -> well-known URI
Client->>RAS: Request (well-known URI): GET server metadata
RAS-->>Client: Response: server metadata (token endpoint, introspection endpoint)
Client->>RAS: Request (Token Endpoint): JWT Authorization Grant w/ ID-JAG
Note over RAS: Verify Request
Note over RAS: Verify ID-JAG is valid format
Note over RAS: Verify if iss in ID-JAG is trusted IdP AS's Issuer URL
Note over RAS: ID-JAG -> iss -> well-known URI
RAS->>IdP: Request (well-known URI): GET server metadata
IdP-->>RAS: Response: server metadata (jwks_uri)
RAS->>IdP: Request (jwks_uri): GET JWKS Set
IdP-->>RAS: Response: JWKS Set
Note over RAS: Verify ID-JAG signature
Note over RAS: Verify ID-JAG: EMA, ID-JAG, RFC 7521, RFC 7523, RFC 6749 specs
RAS-->>Client: Response: JWT Authorization Grant w/ access token
Note over Client: Verify Response
Client->>RAS: Request (Introspection Endpoint): POST introspect w/ access token
RAS-->>Client: Response: JSON document of the access token contents
Note over Client: Verify access token contents
Loading
Implementation description
This lays the groundwork described in the issue's Approach and adds the first set of positive/negative scenarios for a target Resource AS, driven entirely by the ID-JAG flow.
Approach (as implemented)
The test runner plays the MCP Client and hosts a Trusted IdP AS (provideIdPAuthorizationServer.ts). The IdP AS exposes its server-metadata endpoint (well-known URI) and its JWK Set endpoint (jwks_uri), and mints ID-JAGs signed with its own ES256 key.
A scenario is given a settings object (ResourceAuthorizationServerOptions) naming a target Resource AS, plus a details bag through which the runner hands the live IdP AS to the scenario (the IdP's private key is needed to sign ID-JAGs — the settings alone, which carry only issuer URLs, cannot convey it).
The scenario discovers the target Resource AS's token_endpoint and introspection_endpoint by GETting the server metadata at the well-known URI derived from the configured issuer URL.
The scenario mints an ID-JAG (iss = the Trusted IdP issuer, aud = the Resource AS issuer, plus sub/client_id/resource/scope from the settings), presents it as a jwt-bearer authorization grant to the token endpoint (authenticating with client_secret_post), and then introspects the issued access token.
The target Resource AS verifies the ID-JAG by fetching the Trusted IdP metadata at the well-known URI built from the iss claim, reading jwks_uri, and retrieving the signing key — exactly the flow EMA/ID-JAG prescribe.
Configuration (settings the tester provides)
A human tester pre-provisions the target Resource AS and passes the resulting values as ResourceAuthorizationServerOptions (schemas.ts), following the same pattern as AuthorizationServerOptions (issue #208):
MCP Client client_id registered at the Resource AS
3
clientSecret
✅
Client secret (client_secret_post)
4
trustedIdpIssuer
✅
Issuer URL of an IdP the Resource AS trusts
5
untrustedIdpIssuer
✅
Issuer URL of an IdP the Resource AS does not trust
6
trustedMcpServer
⬜
Trusted MCP Server (ID-JAG resource)
7
untrustedMcpServer
⬜
Untrusted MCP Server (ID-JAG resource)
8
scope
⬜
A registered OAuth scope (ID-JAG scope)
9
sub
⬜
A registered user id at the target Resource AS; expected in the sub claim of the issued access token
10
idpSub
⬜
A registered user id at the trusted IdP AS for that same person; used as the ID-JAG sub claim
sub and idpSub are intentionally distinct: the same human may have different ids at the IdP and the Resource AS. The ID-JAG carries idpSub as its sub claim; a Resource AS receiving it is expected to resolve the linked Resource-AS-local user and place that user's sub [9] in the issued access token's sub claim.
Only url is required at the schema level; each scenario validates the subset it needs and emits a SKIPPED check when a required setting is absent. Example: resource-authorization-server-settings.example.json.
Scenarios & checks
Positive
ema/resource-authorization-server/metadata — Resource AS metadata advertises the id-jag grant profile and jwt-bearer grant type (resource-as-metadata-*).
ema/resource-authorization-server/happy-path-with-resource — ID-JAG with a trusted resource; introspection reports a single-valued aud = the MCP Server (resource-as-happy-with-resource-*).
ema/resource-authorization-server/happy-path-with-scope — as above plus a scope; introspection carries it back (resource-as-happy-with-scope-*).
Negative
ema/resource-authorization-server/error-path — ID-JAG resource = an untrusted MCP Server → 400 invalid_target (RFC 8707 §2.1) (resource-as-error-invalid-target-*).
ema/resource-authorization-server/error-path-invalid-scope — trusted resource but an unregistered scope → 400 invalid_scope (RFC 6749 §5.2) (resource-as-error-invalid-scope-*).
ema/resource-authorization-server/error-path-invalid-signature — the trusted IdP signs an otherwise well-formed ID-JAG with a key it never publishes at its own jwks_uri. The Resource AS fetches the IdP's real JWK Set, can't verify the signature, and must reject with 400 invalid_grant (RFC 7521 §4.1.1).
ema/resource-authorization-server/error-path-untrusted-idp — the untrusted IdP AS signs a legitimate, well-formed ID-JAG with its own published key. The Resource AS rejects it purely on iss not being in its trust list — 400 invalid_grant (RFC 7521 §4.1.1) — independent of signature validity. This is the untrusted-IdP negative test called out as missing in the original follow-ups; it uses the untrustedIdpIssuer config field and details channel that were already in place.
Groundwork prototype
ema/resource-authorization-server/idp-metadata-prototype — verifies the runner-hosted Trusted IdP AS publishes discoverable metadata whose jwks_uri serves the key that verifies an ID-JAG it signed (idp-metadata-*, idp-jwks-serves-signing-key).
provideIdPAuthorizationServer.ts — the runner-hosted IdP AS (ES256 keys, ID-JAG mint/verify, metadata + JWKS endpoints). Supports a configurable issuer (with a localUrl getter) so the runner can advertise a stable, externally reachable issuer for real-target runs while binding to a local port.
mockResourceAuthorizationServer.ts — a spec-shaped mock Resource AS used as the test target (ID-JAG validation, audience-restricted token issuance, RFC 7662 introspection). Accepts client_secret_post and client_secret_basic.
src/scenarios/ema/resource-authorization-server/{resource-authorization-metadata,resource-authorization-happy-path,resource-authorization-error-path,prototype}.ts (+ *.test.ts) — the scenarios above.
support.ts — discoverResourceAs(url) (well-known → endpoints) and getIdp(details, key) helpers.
test-harness.ts — stands up the IdP + mock Resource AS, provisions client/user/trusted-MCP-server/scope, and packages them as options + details for the unit tests.
Prefer one scenario with many checks; same check id flips SUCCESS/FAILURE; missing required setting → SKIPPED.
CLI command
Added a resource-authorization-server command (index.ts) and runner (resource-authorization-server.ts), closing the other follow-up item:
npx @modelcontextprotocol/conformance resource-authorization-server \
--url <Issuer URL of Resource AS> \
--client-id <client_id of MCP client that was registered in Resource AS> \
--client-secret <client secret of MCP client that was registered in Resource AS> \
--trusted-idp-issuer <URL of IdP AS that Resource AS trusts> \
--untrusted-idp-issuer <URL of IdP AS that Resource AS does not trust> \
--trusted-mcp-server <URL of MCP server that Resource AS trusts> \
--untrusted-mcp-server <URL of MCP server that Resource AS does not trust> \
--scope <scope in ID-JAG>\
--idp-sub <user ID in IdP AS>
--sub <user ID in Resource AS>\
--scenario <scenario>
The runner hosts the Trusted and Untrusted IdP AS itself and prints their issuer/local URLs; the target Resource AS must be pre-configured (out of band) to trust the "trusted" one before running. --trusted-idp-issuer/--untrusted-idp-issuer let you front either with a stable, externally-reachable URL (e.g. a tunnel) instead of the ephemeral localhost one.
--file <path> accepts a JSON settings file — see resource-authorization-server-settings.example.json.
npx @modelcontextprotocol/conformance list --resource-authorization-server lists all 8 scenarios.
Omit --scenario to run every scenario against the target; each scenario SKIPs gracefully if a setting/detail it needs is absent.
To do / follow-ups
A resource-authorization CLI command + runner (runner/resource-authorization-server.ts) that hosts the Trusted/Untrusted IdP AS at the configured issuer URLs, passes them via details, and registers the scenarios in index.ts. (Currently the scenarios are driven by unit tests against the mock Resource AS.)
A negative scenario exercising the Untrusted IdP AS (untrustedIdpIssuer / item [5]) — an ID-JAG whose iss is untrusted must be rejected. The config field and a details channel for the untrusted IdP are in place, but no scenario uses them yet.
Documentation of the settings a tester must pre-register at the target Resource AS.
MCP Server should do Token Introspection instead of MCP Client because some security-hardened AS only accept token introspection request from an entity that is an access token's audience, namely MCP Server.
Background
There is no conformance test for IdP AS and Resource AS of Enterprise-Managed Authorization. The issue is for adding conformance test for Resource AS of EMA
Strategy
Step-by-Step implementation
The conformance test will be implemented in step-by-step.
Firstly, lay the groundwork for implementing this test and implement some positive and negative tests based on that.
Next, implement all tests covering EMA's requirements for Resource AS.
The issue for the first work.
Scope
Specification References
Roles
There are the following five roles:
Approach
jwks_uri.sequenceDiagram autonumber participant Client as MCP Client<br/>(test runner) participant RAS as Resource AS<br/>(test target) participant IdP as Trusted IdP AS<br/>(test runner) Note over Client,IdP: Create ID-JAG Note over Client: Resource AS Issuer URL -> well-known URI Client->>RAS: Request (well-known URI): GET server metadata RAS-->>Client: Response: server metadata (token endpoint, introspection endpoint) Client->>RAS: Request (Token Endpoint): JWT Authorization Grant w/ ID-JAG Note over RAS: Verify Request Note over RAS: Verify ID-JAG is valid format Note over RAS: Verify if iss in ID-JAG is trusted IdP AS's Issuer URL Note over RAS: ID-JAG -> iss -> well-known URI RAS->>IdP: Request (well-known URI): GET server metadata IdP-->>RAS: Response: server metadata (jwks_uri) RAS->>IdP: Request (jwks_uri): GET JWKS Set IdP-->>RAS: Response: JWKS Set Note over RAS: Verify ID-JAG signature Note over RAS: Verify ID-JAG: EMA, ID-JAG, RFC 7521, RFC 7523, RFC 6749 specs RAS-->>Client: Response: JWT Authorization Grant w/ access token Note over Client: Verify Response Client->>RAS: Request (Introspection Endpoint): POST introspect w/ access token RAS-->>Client: Response: JSON document of the access token contents Note over Client: Verify access token contentsImplementation description
This lays the groundwork described in the issue's Approach and adds the first set of positive/negative scenarios for a target Resource AS, driven entirely by the ID-JAG flow.
Approach (as implemented)
provideIdPAuthorizationServer.ts). The IdP AS exposes its server-metadata endpoint (well-known URI) and its JWK Set endpoint (jwks_uri), and mints ID-JAGs signed with its own ES256 key.ResourceAuthorizationServerOptions) naming a target Resource AS, plus adetailsbag through which the runner hands the live IdP AS to the scenario (the IdP's private key is needed to sign ID-JAGs — the settings alone, which carry only issuer URLs, cannot convey it).token_endpointandintrospection_endpointbyGETting the server metadata at the well-known URI derived from the configured issuer URL.iss= the Trusted IdP issuer,aud= the Resource AS issuer, plussub/client_id/resource/scopefrom the settings), presents it as ajwt-bearerauthorization grant to the token endpoint (authenticating withclient_secret_post), and then introspects the issued access token.issclaim, readingjwks_uri, and retrieving the signing key — exactly the flow EMA/ID-JAG prescribe.Configuration (settings the tester provides)
A human tester pre-provisions the target Resource AS and passes the resulting values as
ResourceAuthorizationServerOptions(schemas.ts), following the same pattern asAuthorizationServerOptions(issue #208):urlclientIdclient_idregistered at the Resource ASclientSecretclient_secret_post)trustedIdpIssueruntrustedIdpIssuertrustedMcpServerresource)untrustedMcpServerresource)scopescope)subsubclaim of the issued access tokenidpSubsubclaimsubandidpSubare intentionally distinct: the same human may have different ids at the IdP and the Resource AS. The ID-JAG carriesidpSubas itssubclaim; a Resource AS receiving it is expected to resolve the linked Resource-AS-local user and place that user'ssub[9] in the issued access token'ssubclaim.Only
urlis required at the schema level; each scenario validates the subset it needs and emits aSKIPPEDcheck when a required setting is absent. Example:resource-authorization-server-settings.example.json.Scenarios & checks
Positive
ema/resource-authorization-server/metadata— Resource AS metadata advertises the id-jag grant profile and jwt-bearer grant type (resource-as-metadata-*).ema/resource-authorization-server/happy-path— ID-JAG withoutresource; token response + introspection (resource-as-happy-*).ema/resource-authorization-server/happy-path-with-resource— ID-JAG with a trustedresource; introspection reports a single-valuedaud= the MCP Server (resource-as-happy-with-resource-*).ema/resource-authorization-server/happy-path-with-scope— as above plus ascope; introspection carries it back (resource-as-happy-with-scope-*).Negative
ema/resource-authorization-server/error-path— ID-JAGresource= an untrusted MCP Server →400 invalid_target(RFC 8707 §2.1) (resource-as-error-invalid-target-*).ema/resource-authorization-server/error-path-invalid-scope— trustedresourcebut an unregisteredscope→400 invalid_scope(RFC 6749 §5.2) (resource-as-error-invalid-scope-*).ema/resource-authorization-server/error-path-invalid-signature— the trusted IdP signs an otherwise well-formed ID-JAG with a key it never publishes at its ownjwks_uri. The Resource AS fetches the IdP's real JWK Set, can't verify the signature, and must reject with400 invalid_grant(RFC 7521 §4.1.1).ema/resource-authorization-server/error-path-untrusted-idp— the untrusted IdP AS signs a legitimate, well-formed ID-JAG with its own published key. The Resource AS rejects it purely onissnot being in its trust list —400 invalid_grant(RFC 7521 §4.1.1) — independent of signature validity. This is the untrusted-IdP negative test called out as missing in the original follow-ups; it uses theuntrustedIdpIssuerconfig field anddetailschannel that were already in place.Groundwork prototype
ema/resource-authorization-server/idp-metadata-prototype— verifies the runner-hosted Trusted IdP AS publishes discoverable metadata whosejwks_uriserves the key that verifies an ID-JAG it signed (idp-metadata-*,idp-jwks-serves-signing-key).Full scenario list
ema/resource-authorization-server/metadataema/resource-authorization-server/happy-pathema/resource-authorization-server/happy-path-with-resourceema/resource-authorization-server/happy-path-with-scopeema/resource-authorization-server/error-pathinvalid_target)ema/resource-authorization-server/error-path-invalid-scopeinvalid_scope)ema/resource-authorization-server/error-path-invalid-signatureinvalid_grant, unpublished key)ema/resource-authorization-server/error-path-untrusted-idpinvalid_grant, untrusted issuer)Implementation notes
New files
provideIdPAuthorizationServer.ts— the runner-hosted IdP AS (ES256 keys, ID-JAG mint/verify, metadata + JWKS endpoints). Supports a configurableissuer(with alocalUrlgetter) so the runner can advertise a stable, externally reachable issuer for real-target runs while binding to a local port.mockResourceAuthorizationServer.ts— a spec-shaped mock Resource AS used as the test target (ID-JAG validation, audience-restricted token issuance, RFC 7662 introspection). Acceptsclient_secret_postandclient_secret_basic.src/scenarios/ema/resource-authorization-server/{resource-authorization-metadata,resource-authorization-happy-path,resource-authorization-error-path,prototype}.ts(+*.test.ts) — the scenarios above.support.ts—discoverResourceAs(url)(well-known → endpoints) andgetIdp(details, key)helpers.test-harness.ts— stands up the IdP + mock Resource AS, provisions client/user/trusted-MCP-server/scope, and packages them asoptions+detailsfor the unit tests.resource-authorization-server-settings.example.json— sample settings file.Modified files
schemas.ts— addResourceAuthorizationServerOptionsSchema/ResourceAuthorizationServerOptions.types.ts— addScenarioForResourceAuthorizationServer(run(options, details)), mirroringClientScenarioForAuthorizationServer.Patterns to follow
AuthorizationServerOptions(Add positive tests for the Authorization Code Grant #208); scenario shaperun(options, details)mirrorsauthorization-code-grant.ts.idflips SUCCESS/FAILURE; missing required setting →SKIPPED.CLI command
Added a
resource-authorization-servercommand (index.ts) and runner (resource-authorization-server.ts), closing the other follow-up item:--trusted-idp-issuer/--untrusted-idp-issuerlet you front either with a stable, externally-reachable URL (e.g. a tunnel) instead of the ephemeral localhost one.--file <path>accepts a JSON settings file — seeresource-authorization-server-settings.example.json.npx @modelcontextprotocol/conformance list --resource-authorization-serverlists all 8 scenarios.--scenarioto run every scenario against the target; each scenarioSKIPs gracefully if a setting/detail it needs is absent.To do / follow-ups
resource-authorizationCLI command + runner (runner/resource-authorization-server.ts) that hosts the Trusted/Untrusted IdP AS at the configured issuer URLs, passes them viadetails, and registers the scenarios inindex.ts. (Currently the scenarios are driven by unit tests against the mock Resource AS.)untrustedIdpIssuer/ item [5]) — an ID-JAG whoseissis untrusted must be rejected. The config field and adetailschannel for the untrusted IdP are in place, but no scenario uses them yet.