Table of Contents
At a glance
Don't have the time to read the entire post? Our human writers will be sad, but we understand. Summarize the post with your preferred LLM here instead.
Demonstrating Proof of Possession (DPoP) is an OAuth 2.0 extension that binds access and refresh tokens to a key pair held by the client that requested them. Defined in RFC 9449, DPoP limits what a token is worth to anyone who copies it. A standard bearer token works for whoever presents it, but a DPoP-bound token only works when the request also carries a fresh signature from the client’s private key, which always stays with the client.
Short lifetimes keep tokens from working for longer than needed, and minimal scopes limit their reach. DPoP narrows who can use them. That makes it especially relevant to AI agents, which often hold tokens for long-running tasks and can expose them through tool calls, tool results, and logs.
At a glance
DPoP binds an OAuth access token to the public key of the client that requested it, recorded in the token as a key thumbprint.
To use a DPoP-bound token, the client sends a newly signed proof with every request, tied to that request’s HTTP method and URL.
A copied DPoP-bound token can’t be used without the client’s private key, while a bearer token works for anyone who holds it.
For AI agents, DPoP lets the signing key stay outside the model’s context, so a token that leaks through a tool call, tool result, or log can’t be used.
DPoP works at the application layer with ordinary key pairs, while mTLS-bound tokens depend on client certificates and the TLS connection.
DPoP vs. bearer tokens: what is proof of possession?
By default, OAuth access tokens are bearer tokens. Possession, meaning having a copy of the token’s value, is the only requirement for their use. As a result, a token copied from a log, a compromised device, or an intercepted request can be used by anyone until it expires. Setting tight expiration and refresh token rotation windows can limit its blast radius, but they don’t change who can use the token.
For how bearer tokens relate to the JSON Web Token (JWT) format, see JWT vs. Bearer Token.
Proof-of-possession tokens add a second requirement: along with the token itself, the presenter must demonstrate control of a key the token is bound to. OAuth has two standard methods for showing this, DPoP and mTLS certificate-bound tokens. For both methods, the “possession” part in “proof of possession” ends up meaning something much narrower than “possessing” a bearer token. The presenter must prove they possess the key (which is never sent, but used to sign the proof), so a copy of what is sent (the token and a single-use proof) won’t satisfy the requirement.
How DPoP works
A DPoP exchange involves three parties: the client, the authorization server that issues tokens, and the resource server that accepts them.

The client generates an asymmetric key pair and keeps the private key.
The client requests a token from the authorization server and includes a DPoP proof signed with its private key.
The authorization server verifies the proof and records a thumbprint of the client’s public key in the token.
The authorization server returns an access token bound to that key.
Before calling the resource server, the client signs a new proof.
The client sends the access token and the new proof together.
The resource server verifies the proof and confirms that its key matches the one the token is bound to.
If both checks pass, the resource server returns the protected resource.
What is a DPoP proof?
A DPoP proof is a short JWT the client creates and signs with its private key. The decoded header of a proof for a token request looks like this:
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "l8tFrhx-34tV3hRICRDY9zCkDlpBhF42UQUfWVAWBFs",
"y": "9VE4jf_Ok_o64zbTTlcuNJajHmt6v9TDVrU0CdvGRDA"
}
}The header carries typ (marking the JWT as a DPoP proof), an asymmetric signing algorithm in alg, and the client’s public key in jwk, written in the JSON Web Key format. Its payload looks like this:
{
"jti": "-BwC3ESc6acc2lTc",
"htm": "POST",
"htu": "https://auth.example.com/token",
"iat": 1793390400
}The payload’s claims carry a unique identifier in jti, the HTTP method in htm, the target in htu, and the creation time in iat. Signing the proof shows that the client holds the private key, and the method and URL tie that proof to a single request.
Getting a bound token
The client sends the proof in a DPoP header with its token request. In the authorization code flow, that request may also carry a Proof Key for Code Exchange (PKCE) verifier: PKCE proves the client redeeming the code is the one that started the flow, and the DPoP proof binds the tokens the request returns to the client’s key.
The authorization server checks the proof’s signature against the embedded public key. It then computes a thumbprint of that key and records it in the issued access token as the jkt member of a cnf (confirmation) claim:
{
"sub": "user_123",
"scope": "orders:read",
"cnf": {
"jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
}
}Every OAuth token response includes a token_type, which is ordinarily Bearer. For a bound token, the authorization server sets it to DPoP, which tells the client the binding took effect and that the token must be presented with DPoP proofs. An authorization server that doesn’t support DPoP ignores the proof and returns an ordinary bearer token.
Using the token
Each request to a protected resource carries the token in the Authorization header under the DPoP scheme, in place of Bearer, along with a new proof:
GET /orders HTTP/1.1
Host: api.example.com
Authorization: DPoP eyJhbGciOiJFUzI1NiIsImtpZCI6...
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2Ii...This proof also includes ath, a hash of the access token, which ties the proof to that specific token. The resource server verifies the proof by checking that:
The signature matches the proof’s public key.
The key’s thumbprint matches the token’s
cnf.jkt.The method and URL match the request.
The proof is recent.
Its
jtihasn’t been used before.The
athvalue matches a hash of the access token presented.
Because the client signs its own proofs, anything with brief access to the private key could sign proofs ahead of time for later use. To prevent that, a server can issue a nonce (“number used once”) in a DPoP-Nonce response header and require the client’s next proof to include it. This means a proof can’t be created before the server issues the nonce.
The key pair
Because the private key is what makes a bound token usable, protecting a bound token comes down to keeping the key inside the client. Browser apps are common DPoP clients, where the Web Crypto API can generate a private key marked non-extractable. Scripts can sign with it but can’t export it.
Each token stays bound to the key it was issued for, and the binding can’t be moved. A public client, such as a browser or mobile app that can’t keep a client secret, has its refresh token bound to the same key. That means losing the key requires running a new authorization flow. A confidential client’s refresh token isn’t bound because the client already authenticates itself at the token endpoint, so it can move to a new key at refresh.
Why DPoP matters for AI agents
AI agents typically use OAuth tokens to reach APIs and MCP servers, either on a user’s behalf or as themselves. An agent working through a long task may hold several tokens at once and present each of them many times. Depending on how the agent is built, those tokens can also end up in tool call arguments, tool results, and the traces and logs that document them.
For bearer tokens, this significantly widens the ways in which they can leak. An agent that follows instructions hidden in a tool description or a fetched document (as in tool poisoning attacks) can be steered into repeating the token in its context. If that’s a bearer token, repeating it hands over access.
DPoP separates the token from the ability to use it. The proof is signed by the code that sends the HTTP request, so the private key can stay out of the model’s context. If a token leaks, it leaks without the key that makes it usable, and a resource server that checks for DPoP refuses it.
When an agent uses OAuth token exchange to get a narrower token for a downstream service, it can send a DPoP proof with the request. This binds the new token to the agent’s own key, so each token in the chain works only for the party holding it.
DPoP compared with bearer and mTLS-bound tokens
Dimension | Bearer token | DPoP-bound token | mTLS certificate-bound token |
|---|---|---|---|
Token is bound to | Nothing | The client’s public key | The client’s TLS certificate |
Proof sent with each request | None | A signed JWT in the DPoP header | The TLS handshake using the client certificate |
A copied token | Works until it expires | Fails without the client’s private key | Fails without the certificate’s private key |
What the client needs | The token | A key pair it generates itself | A certificate and a direct TLS connection to the resource server |
Because a DPoP proof travels inside the HTTP request, the binding remains even when TLS terminates at a proxy or load balancer in front of the resource server, which complicates certificate binding. Clients also generate their own keys, without a certificate authority. That makes DPoP practical for browser apps, mobile apps, and AI agents, in scenarios where provisioning client certificates is difficult.
DPoP with Descope
DPoP covers one part of a wider problem in agentic identity governance: making sure the party presenting a token is the one it was issued to. Neighboring standards mostly cover the things that happen around it. For example, client authentication methods like SPIFFE, private_key_jwt, and mTLS prove who a client is when it requests a token. Delegation standards such as OAuth token exchange and Cross-App Access (XAA) carry a user’s access from one app or agent to the next, and the Personal Agent Protocol (Poppy) extends delegated access to consumer websites. DPoP can apply across all of these, keeping each issued token bound to the key of the client that holds it.
Descope supports DPoP in the Agentic Identity Hub, alongside SPIFFE, private_key_jwt, and mTLS client authentication. For consumer sites, Descope Envoy makes websites Poppy-compliant, and the Poppy JavaScript SDK handles DPoP proofs and bound tokens for the personal agents that connect to them.
To get started, visit the Agentic Identity Hub docs and sign up for a Free Forever Descope account.



