DPoP
Makes a token copied from a log, a trace or a model’s context useless without the key.
Flows / Cross-cutting
Bind a token to a key so that stealing the token is not enough to use it.
A bearer token works like cash: whoever holds it can spend it. That was an acceptable trade when tokens lived in a server’s memory. Agents put tokens in riskier places. They pass through tool calls, logs, traces, proxies and sometimes the model’s own context, and an agent that reads untrusted content can be talked into revealing what it holds.
Demonstrating Proof of Possession fixes the cash problem at the application layer. The agent generates a key pair, and the authorization server binds each token to the public key. Every request then carries a small signed proof made with the private key. A stolen token without the key is rejected.
Mutual TLS does the same job at the transport layer and is a good fit where a service mesh or workload identity system already manages certificates. DPoP works where the agent cannot control TLS: public clients, desktop apps, anything behind a proxy that terminates connections.
7 steps between 3 parties. Each step shows the message and who holds which credential afterward. Use ←→ or select any arrow.
Drag to see every party and step. Tap a step to jump to its explanation.
One key pair per agent instance, kept for the life of the session or longer. The private key should never be readable by the model or written to a log.
Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
Any grant type works. The extra piece is the DPoP header: a JWT that includes the public key and is signed with the private key. It names the HTTP method and URL it is for, and has a unique jti so it cannot be replayed.
POST /token HTTP/1.1
Host: auth.example
Content-Type: application/x-www-form-urlencoded
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7…
grant_type=authorization_code&code=…&code_verifier=…Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
The access token contains a confirmation claim with the thumbprint of the agent’s public key. The response says token_type: DPoP instead of Bearer.
{
"sub": "user:alice",
"aud": "https://calendar.example/api",
"exp": 1794269400,
"cnf": {
"jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
}
}Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
Before each API call the agent signs a new proof. It covers the method (htm), the URL (htu), the time, a fresh jti, and a hash of the access token (ath) so the proof cannot be paired with a different token.
{
"typ": "dpop+jwt", "alg": "ES256",
"jwk": { "kty": "EC", "crv": "P-256", "x": "l8tFrhx-34tV3hRICRDY9zCkDlpBhF42UQUfWVAWBFs", "y": "9VE4jf_Ok_o64zbTTlcuNJajHmt6v9TDVrU0CdvGRDA" }
}
.
{
"jti": "e1j3V_bKic8-LAEB",
"htm": "GET",
"htu": "https://calendar.example/api/events",
"iat": 1794268830,
"ath": "fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo"
}Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
Note the DPoP authorization scheme. A resource server that sees a DPoP-bound token presented as Bearer must reject it.
GET /api/events HTTP/1.1
Host: calendar.example
Authorization: DPoP eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9…
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7…Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
It verifies the proof’s signature with the key inside it, checks that the key’s thumbprint equals cnf.jkt in the token, that method and URL match this request, that the proof is recent, and that jti has not been seen before.
Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
An attacker who copied the access token from a log gets nothing: they cannot produce a proof without the private key, and a captured proof is useless for any other method, URL or moment.
Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
All three prove that the caller holds a key. Mutual TLS (RFC 8705) binds the token to a client certificate; it is strong and invisible to application code, but breaks when something in the path terminates TLS. DPoP signs a small proof per request and survives proxies. HTTP Message Signatures (RFC 9421) sign chosen parts of the request itself, including the body, and are the basis for newer work such as Web Bot Auth and workload proof tokens.
See the full tracker for status and links.
Makes a token copied from a log, a trace or a model’s context useless without the key.
Sender-constraining for agents that already have workload certificates.
The minimum bar for any agent acting as an OAuth client.
How DPoP and related schemes refer to the key a token is bound to.
The baseline. A person signs in, approves specific scopes, and the agent receives a token that says so.
Every agent process gets a cryptographic identity from the infrastructure it runs on, and uses it to authenticate both ends of every connection.
A workload identity that is bound to a key and proven on every request, so it survives the gateways and proxies that agent traffic passes through.
The agent never holds the secret. A proxy outside the sandbox adds it to outgoing requests, and only for the hosts it was meant for.