Skip to content

Client Auth: DPoP Sender-Constrained Tokens (SEP-1932) #368

Description

@PieterKas

Overview

SEP-1932
defines an optional MCP authorization extension that adopts OAuth 2.0
Demonstrating Proof of Possession (RFC 9449)
to make access tokens sender-constrained. A DPoP-bound token cannot be used
by anyone who does not also control the private key the token is bound to, which
mitigates token theft and replay.

This issue covers MCP client conformance only. It validates that a client
obtains a DPoP-bound access token, presents it correctly on every MCP request,
constructs well-formed DPoP proofs, handles server-supplied nonce challenges, and
behaves correctly when the token endpoint or MCP server rejects a request.

Key properties of the client role:

  • The client generates its own key pair and proves possession of the private
    key on every request. Unlike credential-based extensions, the scenario does not
    supply the proof key — generating and using it correctly is itself under test.
  • The proposal adopts DPoP as defined in RFC 9449, without MCP-specific
    extensions or additional proof material.
  • The extension is OPTIONAL and additive; it builds on the baseline MCP
    authorization requirements, which continue to apply unchanged.

Specification References

Scope

In scope — what the MCP client does:

  • Generating a DPoP key pair and using it to sign proofs.
  • Sending a DPoP proof to the token endpoint and obtaining a DPoP-bound token.
  • Presenting the access token with the DPoP Authorization scheme on MCP requests.
  • Constructing a fresh, well-formed DPoP proof per request, including the ath
    claim when an access token is presented.
  • Handling authorization-server and resource-server nonce challenges.
  • Correct error handling when the token endpoint or MCP server rejects a request.

Not in scope (covered elsewhere or by another role):

  • DPoP proof validation logic and the RFC 9449 §4.3 checklist — these are
    resource-server concerns, covered by the server conformance issue.
  • Authorization-server token binding (cnf/jkt), metadata advertisement, and
    token-endpoint nonce issuance behaviour — covered by the authorization-server
    conformance issue.
  • Cryptographic strength policy and signing-key storage — implementation
    concerns, not protocol conformance.

The test authorization server and test MCP server are the judges. They
accept the client's proof at face value for structural and possession checks and
use configurable rejection rules to drive the negative cases. The SDK author
writes the thin conformance client.

Changes Required

Test authorization server (createAuthServer)

  • Accept a DPoP proof on the token request and verify the proof is a well-formed
    dpop+jwt signed by the embedded jwk with an asymmetric alg.
  • Issue a DPoP-bound access token (token_type of DPoP); record the bound key
    thumbprint so the test MCP server can confirm proof/token key agreement.
  • Advertise dpop_signing_alg_values_supported in
    /.well-known/oauth-authorization-server.
  • Configurable rejection rules to drive client behaviour:
    • Respond once with 400 use_dpop_nonce + DPoP-Nonce to exercise the
      token-endpoint nonce retry.
    • Respond with invalid_dpop_proof to exercise client error handling.
  • Append the corresponding conformance checks to the shared checks array.

Test MCP server (createServer)

  • Require the DPoP Authorization scheme; reject Bearer presentation of a
    DPoP-bound token.
  • Verify the per-request proof: required claims present (jti, htm, htu,
    iat), htm/htu match the observed request, ath equals the hash of the
    presented token, signature verifies against the embedded jwk, and jti is
    fresh relative to previously seen proofs.
  • Configurable rejection rules:
    • Issue a 401 with WWW-Authenticate: DPoP ... error="use_dpop_nonce" +
      DPoP-Nonce to exercise the resource-server nonce retry.
    • Issue a 401 invalid_token to exercise client non-fallback behaviour.
  • Append the corresponding conformance checks to the shared checks array.

Helpers (helpers/)

  • DPoP proof parsing/verification helper (decode dpop+jwt, verify signature
    against embedded jwk, extract claims).
  • JWK SHA-256 thumbprint helper (for proof/token key agreement).
  • ath computation helper (base64url(SHA-256(token))).
  • Nonce issuance/tracking helper for both test servers.

Scenario registration (src/scenarios/client/auth/index.ts)

  • Register the new scenario in extensionScenariosList (optional protocol
    extension).

Acceptance test suite

  • Helper unit tests, test-server unit tests (one per rejection rule), and
    scenario acceptance tests that exercise both a conformant and a deliberately
    non-conformant client. See Acceptance Criteria.

Components that do not change

  • The MCP protocol interaction itself is unchanged — the grant/token type does
    not alter the JSON-RPC exchange beyond the Authorization scheme and DPoP
    header, which the test MCP server already needs to observe.
  • OIDC discovery / signing-key resolution infrastructure — authorization-server
    concerns, not client conformance.

Checks to Cover

Positive

  • Client sends a DPoP proof on the token request: typ=dpop+jwt, asymmetric
    alg, embedded public jwk (no private key), htm=POST, htu=token
    endpoint, jti, iat.
  • Client obtains and uses a DPoP-bound access token (does not discard the
    binding and fall back to a bearer flow).
  • Client presents the token to the MCP server with Authorization: DPoP <token>
    (not Bearer).
  • Client includes a DPoP proof header on each MCP request with jti, htm,
    htu, iat.
  • Resource-access proof htm/htu match the actual MCP request method and
    target URI.
  • Resource-access proof includes ath = base64url(SHA-256(access token)).
  • Client generates a fresh proof per request (unique jti, current iat).
  • Client signs with an asymmetric algorithm advertised in
    dpop_signing_alg_values_supported; never none or a symmetric algorithm.
  • Token-endpoint nonce challenge: on 400 use_dpop_nonce + DPoP-Nonce, the
    client retries the token request with a nonce claim matching the supplied
    value.
  • Resource-server nonce challenge: on 401 ... use_dpop_nonce + DPoP-Nonce,
    the client retries the request with a nonce claim matching the supplied
    value.

Negative (client error handling)

  • On invalid_dpop_proof from the token endpoint, the client does not retry
    blindly and surfaces a clear, actionable error.
  • On a non-nonce 401 invalid_token from the MCP server, the client does not
    silently fall back to Bearer or another scheme.
  • Client does not replay a prior proof — each request carries a distinct jti.

Acceptance Criteria

  • A single scenario file in src/scenarios/client/auth/ implements all checks
    above (one scenario, many checks).
  • Scenario registered in extensionScenariosList.
  • Every check has both a passing case (conformant client) and a deliberate
    failing case (non-conformant client) proven by the automated acceptance test
    suite — no check is vacuously passing.
  • Helper unit tests cover proof parsing, thumbprint, ath, and nonce helpers.
  • Test-server unit tests cover each rejection rule (correct status, body, and
    recorded check ID/status).
  • The acceptance test suite runs as part of npm test.
  • Scenario runs through the standard CLI runner
    (npx @modelcontextprotocol/conformance client --command "..." --scenario ...);
    no parallel entry point is introduced.
  • Validated against at least one real SDK conformance client before the PR is
    submitted; SDK baseline YAMLs updated where existing SDKs do not yet support
    DPoP.

Out of Scope

  • Required jti state-tracking — optional and stateful per RFC 9449 §11.1; not a
    client behaviour.
  • Authorization-server and resource-server validation behaviour — covered by the
    separate server and authorization-server conformance issues.

Notes

Prepared with the aid of Claude (Opus 4.8)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions