Skip to content

Add initial conformance tests for Resource AS of EMA #470

Description

@tnorimat

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

  • 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.

Specification References

Roles

There are the following five roles:

# Role Player Description
1 Resource AS Tester The test target of the conformance test
2 MCP Client Test Runner The MCP Client that sends ID-JAG to an IdP AS
3 Trusted IdP AS Test Runner The IdP AS that the Resource AS trusts
4 Untrusted IdP AS Test Runner The IdP AS that the Resource AS does not trust
5 Trusted MCP Server Test Runner The MCP Server that the Resource AS trusts
6 Untrusted MCP Server Test Runner 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):

# Setting Req. Purpose
1 url Resource AS issuer URL; well-known metadata → token & introspection endpoints
2 clientId 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 — ID-JAG without resource; token response + introspection (resource-as-happy-*).
  • 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 scope400 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).

Full scenario list

# Scenario Kind
1 ema/resource-authorization-server/metadata positive (discovery)
2 ema/resource-authorization-server/happy-path positive
3 ema/resource-authorization-server/happy-path-with-resource positive
4 ema/resource-authorization-server/happy-path-with-scope positive
5 ema/resource-authorization-server/error-path negative (invalid_target)
6 ema/resource-authorization-server/error-path-invalid-scope negative (invalid_scope)
7 ema/resource-authorization-server/error-path-invalid-signature negative (invalid_grant, unpublished key)
8 ema/resource-authorization-server/error-path-untrusted-idp negative (invalid_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 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.tsdiscoverResourceAs(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.
  • resource-authorization-server-settings.example.json — sample settings file.

Modified files

  • schemas.ts — add ResourceAuthorizationServerOptionsSchema / ResourceAuthorizationServerOptions.
  • types.ts — add ScenarioForResourceAuthorizationServer (run(options, details)), mirroring ClientScenarioForAuthorizationServer.

Patterns to follow

  • Settings modeled on AuthorizationServerOptions (Add positive tests for the Authorization Code Grant #208); scenario shape run(options, details) mirrors authorization-code-grant.ts.
  • 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions