Sender-Constrained Tokens for MCP Servers: DPoP and mTLS

The MCP authorization specification builds on OAuth 2.1, and OAuth 2.1 issues bearer tokens. A bearer token is usable by whoever holds it. That is the whole definition. If a token issued to an agent host is copied out of a log, a crash dump, a browser extension, or a developer laptop, it works from anywhere until it expires or is revoked.

MCP makes this more concrete than a normal API does. The credential lives in an agent host you do not control, on a machine you do not manage, and it is used by software that reads untrusted text all day. Short lifetimes and revocation reduce the window. Sender-constrained tokens remove the copied token as a usable credential at all.

This is a guide to the two mechanisms a reviewer will recognise, what your MCP server has to do for each, and when the honest answer is that you do not need this yet.

What sender-constraining means

A sender-constrained access token is cryptographically tied to a key held by the client. The token alone is not enough; the caller must also prove possession of the key on every request. Two mechanisms are standardised and deployed:

  • DPoP, Demonstrating Proof of Possession at the Application Layer, RFC 9449. The client generates a key pair, sends the public key to the authorization server when it requests a token, and then signs a small JWT proof for each HTTP request. Application layer, works over ordinary TLS.
  • Mutual TLS client certificates, RFC 8705. The client authenticates the TLS connection with a certificate, and the issued token carries a confirmation claim containing the certificate thumbprint. Transport layer, requires certificate distribution.

Both end up in the same place in the token: a cnf (confirmation) claim. For DPoP it is cnf.jkt, the thumbprint of the client's public JSON Web Key. For mTLS it is cnf.x5t#S256, the thumbprint of the client certificate. Your server's job is to check that the presented proof matches the thumbprint in the token it just validated.

RFC 9700, the OAuth security best current practice, recommends sender-constraining access tokens. It is a recommendation, not a requirement of the MCP specification. Nothing in MCP forbids plain bearer tokens, and most MCP deployments today use them.

What the MCP server has to verify for DPoP

DPoP moves work to the resource server. If you support it, the tool-call path gains these checks, in addition to the signature, issuer, expiry, and audience validation you already do:

  1. The Authorization header uses the DPoP scheme rather than Bearer.
  2. A DPoP header is present and contains a JWT whose header carries the client's public key in jwk, with a typ of dpop+jwt.
  3. The proof's signature verifies against that embedded public key.
  4. The SHA-256 thumbprint of that public key equals the cnf.jkt claim in the access token. This is the binding. Without this comparison, DPoP proves nothing.
  5. The proof's htm and htu claims match the HTTP method and the request URI as your server sees it, normalised and without query or fragment.
  6. The proof's ath claim equals the SHA-256 hash of the access token, so a proof cannot be paired with a different token.
  7. The proof's iat is inside a narrow window, and its jti has not been seen before within that window.

Item 7 is the part teams underestimate. Replay protection needs shared, short-lived storage for jti values across every instance behind your load balancer. A local in-process cache silently fails to protect anything the moment you run two replicas. Redis with a TTL slightly longer than your accepted clock skew is the usual answer, and it is a new dependency on the hot path of every tool call.

Two more details that cause support tickets. First, htu must be computed from the externally visible URL, so a proxy that rewrites the host or strips the path prefix will break every proof until you honour the forwarded headers. Second, when a client presents a valid token with a missing or bad proof, respond 401 with WWW-Authenticate: DPoP and an error="use_dpop_nonce" or invalid_dpop_proof value as appropriate, so the client can correct itself rather than retrying blindly. If you require server-provided nonces, you also have to issue them via the DPoP-Nonce header and accept a grace period while clients rotate.

What the MCP server has to verify for mTLS

mTLS is less code and more logistics. The TLS terminator validates the client certificate; your application compares the SHA-256 thumbprint of the presented leaf certificate to cnf.x5t#S256 in the access token. There is no per-request signature to verify, no nonce, no replay window, and no new datastore.

The cost is elsewhere. Termination usually happens at a load balancer or ingress, so the certificate has to reach your application intact, normally as a forwarded header that only the terminator may set. If any hop can set that header, you have replaced a token theft problem with a header spoofing problem. Beyond that, certificates have to be issued, distributed, and rotated for every client, which is workable for a handful of known enterprise callers and unworkable for a public connector directory listing where clients register dynamically.

A reasonable split: mTLS for a small set of named, contractual clients; DPoP for anything self-registering.

What this does not fix

Sender-constraining defends against a token that has been copied and used elsewhere. It does nothing about the following, and a reviewer who knows the mechanism will ask:

  • Malicious or compromised code inside the legitimate client. Whatever holds the key can mint proofs. If the agent host is compromised, DPoP signs the attacker's requests too.
  • Prompt injection. The token is the right token, the proof is valid, and the call is still one the user never intended. That is a tool design and scoping problem, not a token problem.
  • Over-broad scopes. A well-bound token that grants tenant-wide read access still grants tenant-wide read access.
  • Missing per-user enforcement. Identity from the token has to be enforced at the data layer either way.

Sender-constraining also does not remove the need for short lifetimes, refresh token rotation, or revocation. It changes what a leaked token is worth, not what an active session can do.

When to do this, and when not to

The order that actually reduces risk on most MCP servers we look at is: audience validation per RFC 8707 and protected resource metadata per RFC 9728 first, then server-enforced per-user and per-tenant scoping, then narrow tools with explicit write classification, then structured audit events, then token binding. A server that passes an unbound token to a downstream API has a confused deputy problem that DPoP will not touch.

There are two situations where we would raise token binding early. One is a client requirement: some enterprise security teams and some regulated environments ask for proof-of-possession credentials directly, and the answer is either yes or a long exception process. The other is a deployment where the token reaches a place you consider hostile, such as a desktop agent host with third-party extensions.

Check your identity provider before you plan any of it. DPoP and mTLS-bound token support at the authorization server is uneven, and the resource server side is pointless without a cnf claim in the token. If your provider cannot issue bound tokens for the application registration you use for MCP, the work is a provider migration, not a server change. That is a different conversation and a much larger one.

What to write in the questionnaire

If you have implemented it, be specific about the mechanism and the checks, because "we use proof of possession" invites follow-up questions:

Access tokens for the MCP server are DPoP-bound per RFC 9449. The server validates the proof signature, confirms the JWK thumbprint against the cnf.jkt claim in the access token, binds the proof to the method and URI, verifies the ath claim against the token hash, and rejects replayed jti values within a 60-second window using shared storage. An exfiltrated access token is not usable without the client's private key.

If you have not implemented it, say that plainly and say what you do instead: token lifetime with a number, refresh token rotation, revocation propagation with a number, and per-user enforcement at the data layer. A named gap with compensating controls reads better in a review than a vague claim.

If you are deciding whether token binding belongs in your next MCP milestone or three items further down, that is the kind of question our MCP security audits answer in writing, against your provider and your deployment rather than in general. If your provider already issues bound tokens and you only need the resource server side built correctly, that is a small piece of work and we will tell you so in week one.