What Is DPoP (RFC 9449)? Sender-Constrained Tokens for AI Agents Explained
DPoP (RFC 9449) ties an OAuth access token to a key so a stolen token fails: proof JWTs, cnf jkt, server nonces, and how PAP, FAPI 2.0 and MCP servers use it.
DPoP, short for Demonstrating Proof of Possession, is defined in RFC 9449. The RFC's abstract calls it "a mechanism for sender-constraining OAuth 2.0 tokens via a proof-of-possession mechanism on the application level." In plain terms: an access token stops being a password-like string that works for whoever holds it, and becomes a token that only works together with a private key the legitimate client keeps.
The problem: bearer tokens
A normal OAuth access token is a bearer token. Anyone who gets a copy can use it until it expires. Tokens leak through logs, error reports, URLs, browser extensions and compromised servers. This is a particular concern for AI agents, whose tokens can pass through prompts, tool outputs, transcripts and telemetry. DPoP does not stop a leak. It makes a leaked token far less useful.
How it works, step by step
- The client makes a key pair and keeps the private key.
- For each request, it signs a short proof (a JWT) and sends it in a
DPoPheader. - The authorization server binds the token to the key. The token response has
token_type: "DPoP", and the access token is tied to the public key through a confirmation claim,cnf.jkt, which is a thumbprint of that key (RFC 9449 section 6.1). - The client calls the API with
Authorization: DPoP <token>plus a fresh proof. - The resource server checks that the proof is valid, that its key matches the one the token is bound to, and that the proof matches this request.
A thief with only the token cannot create a valid proof, so the token fails.
What is inside a proof
The proof is a JWT with a header and claims (RFC 9449 section 4.2):
| Part | Value | Why |
|---|---|---|
Header typ | dpop+jwt | Marks it as a DPoP proof |
Header alg | An asymmetric algorithm, never none | The signature type |
Header jwk | The client's public key | Lets the server verify the signature and compute the thumbprint |
jti | A unique ID with at least 96 bits of randomness | Lets servers reject a replayed proof |
htm | The HTTP method | Ties the proof to this request type |
htu | The target URL, without query or fragment | Ties the proof to this endpoint, so it cannot be reused elsewhere |
iat | Creation time | Keeps proofs fresh |
ath | Base64url SHA-256 hash of the access token | Present when calling an API; ties the proof to one token |
nonce | A server-provided value | Present when the server asks for one |
An illustrative decoded proof for an API call (all values fictional):
{
"header": { "typ": "dpop+jwt", "alg": "ES256", "jwk": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" } },
"claims": {
"jti": "f3a9c1d2-7b64-4e0a-9d55-2c8b1e7a0001",
"htm": "GET",
"htu": "https://api.example.test/orders/1042",
"iat": 1791555000,
"ath": "fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo"
}
}
Server nonces
A client can sign proofs ahead of time, or an attacker who controls the client can generate them for the future. To prevent that, a server may require a nonce it chooses. The RFC describes the flow: an authorization server replies with HTTP 400, the error use_dpop_nonce and a DPoP-Nonce header (a resource server uses a 401 challenge instead); the client retries with that value in the proof's nonce claim (section 8). Invalid proofs get invalid_dpop_proof.
Where DPoP appears in the agent world
- Personal Agent Protocol. The PAP Draft 0.1 specification makes its short-lived session tokens bound to a DPoP key. Its one exception is MCP servers, where it allows bearer tokens and flags them as less secure. See what PAP is.
- FAPI 2.0. The OpenID Foundation's FAPI 2.0 Security Profile requires sender-constrained access tokens, using either mutual TLS (RFC 8705) or DPoP. See FAPI and agent checkout.
- OAuth metadata. A server advertises support with
dpop_signing_alg_values_supportedin its authorization server metadata (RFC 9449 section 5.1). A protected resource can publishdpop_bound_access_tokens_requiredin its RFC 9728 metadata to say it always requires DPoP-bound tokens.
DPoP vs plain bearer vs mutual TLS
| Bearer token | DPoP | Mutual-TLS-bound token | |
|---|---|---|---|
| Stolen token alone is usable | Yes | No | No |
| Binding is to | Nothing | A key held by the client | A client certificate |
| Works at application level | Yes | Yes | Needs TLS-level client certificates |
| Server field to look for | none | dpop_signing_alg_values_supported | tls_client_certificate_bound_access_tokens |
What DPoP does not do
The RFC is direct about limits:
- It is "not a substitute for a secure transport" and must always be used with HTTPS.
- It does not protect a client whose private key is stolen, or malicious code running inside the client. Code in the client's context can create proofs, including future-dated ones, and pre-generated proofs are possible. Server-chosen nonces help.
- It does not protect request integrity. A proof covers the method and URL, not the body or other headers (section 11.7).
How to check a server
Look at what an authorization server publishes. The OIDC Inspector has a "FAPI 2.0 Metadata Signals" card that reports whether dpop_signing_alg_values_supported and tls_client_certificate_bound_access_tokens are published. For an OAuth-protected MCP server, the Agent Protocol Inspector shows the same signals for its authorization server. Two cautions: a field that is not published does not prove the capability is missing, and a published field is a claim, not proof the server enforces it. Neither tool tests DPoP behaviour or performs a conformance check.
Sources
Follow Trango Compute on LinkedIn
We post updates on new tools, context engineering patterns, and LLM cost research.