All posts
SECURITY
DPoP for MCP servers — cover art

DPoP for MCP servers: what it protects, what it costs

Bearer tokens treat whoever holds the token as authorized. For agents that copy tokens across processes, logs, and tool calls, that assumption is the problem. DPoP fixes one part of it, and gets credited for several it doesn't touch.

TL;DR

  • A bearer token is a house key: whoever holds it gets in. DPoP (RFC 9449) binds the token to a key the client proves it holds, so a copied token on its own stops working.
  • That matters for agents, which spread tokens across sub-agents, logs, and tool calls in ways a single browser session never did.
  • DPoP isn't free (per-request signing, state to track replay), and it isn't agent identity or delegation. It answers one question: is this the client the token was issued to?

An OAuth bearer token works like a house key. Whoever holds it gets in, and the lock doesn't care how they got it. That's fine when one browser session holds one token behind TLS. It's a different situation when an autonomous agent passes tokens between sub-agents, writes them to logs, and hands them to a dozen tools an hour. Each of those is a place a token can leak, and with bearer semantics a leaked token is a working credential until it expires.

DPoP (Demonstrating Proof of Possession, RFC 9449) changes the acceptance rule. Instead of "present the token," it becomes "present the token and prove you hold the key it's bound to." A copied token, by itself, stops working. It's easy to credit DPoP with more than it does, so precision helps: it solves one problem well, and gets blamed or praised for several others it never touches.

What DPoP actually does

The client generates a key pair and keeps the private key. On each request it sends a DPoP proof: a short JWT signed with that private key, carrying the HTTP method (htm), the request URI minus its query string and fragment (htu), a timestamp (iat), a unique ID (jti), and, once it has an access token, a hash of it (ath). The authorization server binds the issued token to the thumbprint of the public key. The resource server then checks, on every call, that the proof is signed by the key the token is bound to and that the method and path match.

The security boundary is simple to state. Holding the token is no longer enough. You also need the private key, and the private key never leaves the client. Pull a token out of a log and you still can't use it, because you can't sign a fresh proof for it.

Why agents change the threat model

Bearer semantics were settled when a token usually lived in one place for one session, and the main risk was that place being stolen. Agents break those assumptions in specific ways:

Tokens fan out. A planner agent hands tokens to sub-agents and tools, multiplying the copies sitting in memory and moving over the wire.

Tokens land in logs. Agent frameworks log requests, traces, and tool calls freely, and tokens end up in plaintext logs and observability pipelines that a lot of people can read.

Workflows run long. An agent can hold a token for hours, which widens the window in which a leaked copy is still good.

DPoP addresses the shared root cause: it makes a copied token inert. Wherever the token leaks, the leak is worth much less, because the copy can't sign proofs, and that's the reason to turn it on.

What DPoP protects, and what it doesn't

ThreatBearer tokenDPoP
Token copied from a log or traceWorks for the attackerUseless without the key
Token replayed to a different endpointWorksRejected (htu binds method and path)
Traffic intercepted in transit (TLS stripped or terminated by an attacker)Works indefinitelyLimited: the attacker captures the proof too, but it only works for that method and path, for the length of the acceptance window
Agent process fully compromised, key includedWorksAlso works: this is the limit

That last row is where DPoP stops. DPoP binds the token to a key. If an attacker compromises the agent process and takes the key along with the token, DPoP gives you nothing, because now they can sign proofs too. It protects against copied tokens, not against a fully compromised holder. Anyone pitching DPoP as protection against a rogue agent is overselling it.

Two rows deserve a footnote. First, DPoP is not a replacement for TLS. RFC 9449 says it "MUST always be used in conjunction with HTTPS," and it leans on TLS for message integrity. An attacker who can read your traffic captures the proof along with the token; what DPoP buys you there is that the captured pair is scoped to one method and path and expires in seconds, instead of being a working credential until the token does. Second, note that htu covers the path, not the query string. A proof captured for one call isn't tied to the specific parameters of that call, only to the method and endpoint.

The partial-compromise case, and what the nonce is for

There's a case between "token leaked" and "key stolen." An attacker with brief code execution inside the agent can mint DPoP proofs dated well into the future, exfiltrate them alongside a token, and keep using the pair long after losing access. Nothing in the base mechanism stops this, because the proofs are genuinely signed by the right key.

The fix is a server-issued nonce. The server hands the client an unpredictable value it must include in the proof, which means proofs can't be minted for a future the attacker can't see. If your threat model includes agents that might be transiently compromised, and for anything running third-party tool code it should, the nonce is the part that matters, not the signature.

What it costs to run

DPoP isn't free. The costs are real, and you should measure them before you enable it everywhere.

A signature on every request. Each call adds a private-key signature on the client and a verification on the server. With ES256 or EdDSA that's small per call, but an agent making thousands of calls an hour multiplies it. Watch tail latency, not just the average, since the slow requests are what stall a workflow.

Replay protection needs state somewhere. A proof has to be single-use within its window, and there are two ways to get there. Track recently seen jti values: simple, but the cache grows with request rate and has to be shared across every server instance or the protection has holes. Or keep the acceptance window short and have the server issue nonces, which bounds the state by nonce lifetime instead of by traffic. Either way there's shared state to operate; the second approach just makes it a fixed cost rather than one that scales with your busiest agent.

Clock skew. Proofs carry a timestamp and are valid only briefly. If agent and server clocks drift apart, legitimate requests start failing. Choose the tolerance window on purpose rather than inheriting a default.

Where the key lives. A private key in an environment variable is a private key in every crash dump and log line. The key's protection is the whole security of the scheme, so it deserves more care than the token ever did.

Where the key should live

The tempting default is one key per agent, held in the agent's own process. It's also the weakest, because compromising the process hands over the key. A stronger pattern holds signing keys at a trust boundary the agent calls into, rather than scattered inside every agent, and mints short-lived, sender-constrained tokens for each hop. A single compromised agent then can't walk away with a reusable key.

Be clear-eyed about what moves when you do this: the DPoP proof now attests that the boundary holds the key, not that any particular agent does, so the agent-to-boundary hop needs its own authentication and its own identity claim. You're trading a possession proof about many processes for a possession proof about one service you can actually harden. Usually worth it, but it's a trade, not a free upgrade. If you're running many agents, build that boundary before you turn DPoP on across the fleet.

DPoP is not delegation, and it's not identity

This is where teams over-rely on it. DPoP proves exactly one thing: the caller holds the key the token is bound to. It says nothing about which user authorized the action, which agent is calling, or who's acting on whose behalf. Those are separate problems with separate answers:

Identity: which agent is this? That needs an explicit agent identifier on the token, not a proof of key possession.

Delegation: on whose behalf, and through what chain? That's RFC 8693 token exchange and actor claims, tracking who handed work to whom.

DPoP sits underneath both. A complete agent-auth setup uses DPoP to stop token theft, an agent identity to say who's calling, and a delegation chain to say for whom. AuthPlane models these as separate layers for exactly this reason: DPoP for possession, an explicit agent_id for identity, and an RFC 8693 chain for delegation.

When should you use DPoP for an MCP server?

Turn it on when your MCP server is remote, when tokens pass through agent frameworks that log or fan them out, or when a leaked token would be a real incident rather than a shrug. Leave it off when you're local and single-agent behind TLS, with short-lived tokens and no logging exposure, because bearer is already enough there and DPoP would be cost without benefit.

AuthPlane supports DPoP (RFC 9449) for sender-constrained tokens, off by default, with a 60-second proof window and an optional server-issued nonce backed by a persisted store, so you can enable it where it earns its cost and leave it off where it doesn't. The point isn't to sign every request everywhere. It's to make a stolen token worthless in the places agents actually leak them.

Sources

Primary and official sources, accessed August 2026.

  • RFC 9449, OAuth 2.0 Demonstrating Proof of Possession (DPoP). Fett, Campbell, Bradley, Lodderstedt, Jones, Waite, September 2023. datatracker.ietf.org/doc/html/rfc9449
  • RFC 9700, Best Current Practice for OAuth 2.0 Security. Lodderstedt, Bradley, Labunets, Fett, January 2025. datatracker.ietf.org/doc/html/rfc9700
  • RFC 8705, OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens. Campbell, Bradley, Sakimura, Lodderstedt, February 2020. datatracker.ietf.org/doc/html/rfc8705
  • RFC 6750, OAuth 2.0 Authorization Framework: Bearer Token Usage. Jones, Hardt, October 2012. datatracker.ietf.org/doc/html/rfc6750
  • RFC 7519, JSON Web Token (JWT). Jones, Bradley, Sakimura, May 2015. datatracker.ietf.org/doc/html/rfc7519
  • A. Goswami, Agentic JWT: A Secure Delegation Protocol for Autonomous AI Agents, arXiv:2509.13597, September 2025. arxiv.org/abs/2509.13597