A bearer access token works for whoever holds it, so a token copied from a log, a proxy, a browser or a compromised integration can be replayed from anywhere until it expires. DPoP (Demonstrating Proof of Possession, RFC 9449) is the OAuth 2.0 extension that binds an access token to a key pair the client holds, so a stolen token is useless without the private key. It does this at the application layer with signed JWTs in an HTTP header, which makes it usable where mutual TLS (RFC 8705) is impractical: browser apps, mobile apps and deployments behind TLS-terminating proxies. This guide explains what DPoP is, how the proof and the binding work, what it does and does not protect against, and how it compares with mTLS. It is written for architects, API and identity engineers, and security reviewers.
Key takeaways
- DPoP turns bearer tokens into sender-constrained tokens. The authorisation server binds the token to the thumbprint of the client's public key, and every API call must carry a fresh proof signed with the matching private key.
- It is the application-layer alternative to mTLS. RFC 8705 binds tokens to a TLS client certificate; DPoP binds them to a JWK and works through proxies, CDNs and in browsers where client certificates are painful.
- Standards now expect sender-constraining. The OAuth 2.0 Security BCP (RFC 9700) says servers should sender-constrain access tokens, and FAPI 2.0 issues only mTLS or DPoP bound tokens.
- Resource servers must enforce it, or it does nothing. An API that accepts a DPoP-bound token as a plain Bearer token, or skips the
ath,htmandhtuchecks, loses the protection entirely. - DPoP does not stop code running inside the client. XSS or malware that can call the signing key can mint valid proofs; server nonces, short token lifetimes and non-extractable keys narrow that window.
What it is
DPoP is defined in RFC 9449, "OAuth 2.0 Demonstrating Proof of Possession (DPoP)", published by the IETF in September 2023 on the Standards Track. It adds three things to OAuth 2.0:
- A proof JWT: a short-lived JWT the client signs with its private key and sends in a
DPoPHTTP header on token requests and API calls. Its header hastypset todpop+jwtand carries the client's public key injwk. - A token binding: the authorisation server records the SHA-256 JWK thumbprint (RFC 7638) of that public key in the access token's confirmation claim,
cnf.jkt, or returns it from token introspection. - A new token type and scheme: the token response returns
token_typeDPoP, and the client presents the token asAuthorization: DPoP <token>rather thanBearer.
The key pair is generated by the client and never registered in advance, so DPoP also works for public clients (single-page apps, native apps) that cannot keep a client secret. For background on the base protocol, see the OAuth 2.0 and OpenID Connect Guide.
How it works
The proof JWT
| Field | Where | Purpose |
|---|---|---|
typ = dpop+jwt | JOSE header | Explicit typing so a proof cannot be confused with another JWT |
alg | JOSE header | Must be an asymmetric algorithm; none and MAC algorithms (HS256) are not allowed |
jwk | JOSE header | The client's public key; must not contain private key material |
jti | Claim | Unique identifier (at least 96 bits of randomness or a UUIDv4) so servers can detect replay |
htm | Claim | HTTP method of the request the proof is for |
htu | Claim | Target URI without query or fragment |
iat | Claim | Creation time; servers accept only a short window |
ath | Claim (API calls) | Base64url SHA-256 hash of the access token, tying the proof to that token |
nonce | Claim (when issued) | Server-supplied value from the DPoP-Nonce header, limiting pre-generated proofs |
The flow
- Key generation: the client creates an asymmetric key pair (commonly ECDSA P-256, ES256). In a browser the private key can be a non-extractable WebCrypto key; on mobile, a hardware-backed keystore key.
- Optional code binding: the client can send
dpop_jkt(the key thumbprint) in the authorisation request, alongside PKCE, or with a pushed authorisation request (PAR), so the authorisation code can only be redeemed with that key (RFC 9449 Section 10). - Token request: the client calls the token endpoint with a
DPoPproof (htmPOST,htuthe token endpoint). All grant types, includingrefresh_token, carry a proof. An invalid proof returnsinvalid_dpop_proof. - Binding: the authorisation server returns
token_type: DPoPand binds the token withcnf.jkt. A response withtoken_type: Bearermeans the token is not bound, and the client should treat that as a configuration fault if it expected binding. - API call: for each request the client signs a new proof with
htm,htu, a freshjti,iatandath, and sends it withAuthorization: DPoP <token>. - Verification: the resource server checks the proof (Section 4.3: one header, well-formed JWT, valid
typandalg, signature againstjwk, matchinghtmand normalisedhtu, acceptableiat, nonce if issued), then checks thatathmatches the token and the proof key thumbprint matchescnf.jkt.
Nonces and refresh tokens
- Nonces: an authorisation server can reply HTTP 400 with error
use_dpop_nonceand aDPoP-Nonceheader; a resource server replies 401 withWWW-Authenticate: DPoP error="use_dpop_nonce". The client retries with the nonce in its proof. Once a server has issued a nonce it must not accept proofs without one. - Refresh tokens: for public clients, refresh tokens are bound to the DPoP key and must be presented with a proof from the same key. Confidential clients' refresh tokens are not DPoP-bound because client authentication already ties them to the client. This matters because RFC 9700 Section 2.2.2 requires public client refresh tokens to be sender-constrained or rotated.
- Metadata: the authorisation server advertises
dpop_signing_alg_values_supported; a client registered withdpop_bound_access_tokens: truehas token requests without a DPoP header rejected. - Browsers: a cross-origin API must list
DPoP-NonceandWWW-AuthenticateinAccess-Control-Expose-Headersor the browser client cannot read the challenge.
DPoP compared with mTLS
| Aspect | DPoP (RFC 9449) | mTLS bound tokens (RFC 8705) |
|---|---|---|
| Layer | Application (HTTP header, signed JWT) | Transport (TLS client certificate) |
| Binding claim | cnf.jkt (JWK SHA-256 thumbprint) | cnf.x5t#S256 (certificate SHA-256 thumbprint) |
| Key provisioning | Client generates a key pair at runtime, no PKI | CA-issued (tls_client_auth) or registered self-signed (self_signed_tls_client_auth) certificates |
| Browser and SPA fit | Good, with WebCrypto non-extractable keys | Poor: client certificate prompts and management |
| TLS-terminating proxies and CDNs | Unaffected, the proof travels in a header | Certificate must be forwarded to the app; RFC 8705 Section 6.5 leaves how to do this unspecified |
| Client authentication | Not a client authentication method; combine with private_key_jwt or a secret if needed | Can also authenticate the client |
| Per-request cost | One signature per request, verification on every call | Handshake cost; no per-request signing |
| Replay window | Bounded by iat window, jti tracking and nonces | Bound to the TLS session key; no proof replay concept |
| Typical fit | Public clients, SaaS APIs, AI agents calling third-party APIs | Service-to-service inside a service mesh or private PKI, open banking |
Choose mTLS where you already run a workload PKI or service mesh and control both ends; choose DPoP where clients are browsers, mobile apps or third parties, or where TLS terminates before your application. FAPI 2.0 (Final, February 2025) accepts either. See the mTLS Guide and the OAuth 2.1, GNAP and FAPI Guide.
Rolling it out
- Confirm support: check the authorisation server's discovery document for
dpop_signing_alg_values_supportedand the client library's DPoP support. Pick ES256 unless a profile requires otherwise. - Make APIs DPoP-aware first: resource servers accept both
BearerandDPoPschemes, validate proofs fully and log the scheme used. A token carryingcnf.jktmust always be rejected when presented as Bearer. - Pilot one client: enable DPoP for a single public client, log
invalid_dpop_proofand nonce retries, and watch clock skew failures oniat. - Enforce per client: set
dpop_bound_access_tokens: trueso the authorisation server refuses unbound requests, then shorten access token lifetimes. - Add nonces where risk is high: enable server nonces for high-value APIs and for clients running in exposed environments.
- Retire Bearer: once logs show no Bearer use for that audience, stop accepting the Bearer scheme. Rollback is per client: unset the enforcement flag, and resource servers keep accepting Bearer until you remove it.
Why it matters
Token theft is a routine attack path, not an edge case. MITRE ATT&CK tracks it as T1528 Steal Application Access Token, with replay of the stolen token as T1550.001 Application Access Token. Bearer tokens leak through browser storage, logs, debugging proxies, compromised SaaS integrations and developer machines. In the Salesloft Drift breach, OAuth tokens stored in the integration's cloud environment were taken and replayed against Salesforce APIs, with Google reporting over 700 potentially affected organisations. With sender-constrained tokens the attacker would also have needed the signing keys, which raises the bar considerably when those keys sit in a KMS or HSM rather than next to the tokens. The same logic applies to AI agents, which hold tokens for many APIs and run in environments that are hard to inspect.
Framework mapping
| Framework | Reference | How DPoP helps |
|---|---|---|
| NIST SP 800-53 Rev. 5 | IA-2(8) Access to Accounts: Replay Resistant | Signed, time-bound, unique proofs resist replay of captured credentials |
| NIST SP 800-53 Rev. 5 | SC-23 Session Authenticity | Each request proves possession of the key bound to the session's token |
| NIST SP 800-53 Rev. 5 | SC-8 Transmission Confidentiality and Integrity | DPoP complements but does not replace TLS (RFC 9449 Section 11) |
| MITRE ATT&CK | T1528, T1550.001 | Stolen tokens cannot be used without the private key |
| IETF RFC 9700 (BCP 240) | Sections 2.2.1, 2.2.2, 4.10.1 | Implements the recommended sender-constraining of access and refresh tokens |
| FAPI 2.0 Security Profile | Sender-constrained tokens | One of the two permitted binding mechanisms |
What to monitor
- Bearer use of bound tokens: any token with
cnf.jktpresented under the Bearer scheme is either a misconfigured client or a replay attempt. Alert on it. - Proof failures: spikes in
invalid_dpop_proofby client, especiallyathor key thumbprint mismatches, indicate token reuse with the wrong key. - Replayed
jti: repeats within the acceptance window. - Metrics: percentage of API calls using DPoP-bound tokens per audience; number of clients with
dpop_bound_access_tokensenforced; Bearer acceptance remaining per API; median access token lifetime for bound versus unbound clients; nonce retry rate (a health signal for client libraries).
Common misconceptions
- "DPoP authenticates the client." It does not. The key is self-generated and proves continuity, not identity. Confidential clients still need client authentication, ideally
private_key_jwtor mTLS. - "DPoP replaces TLS." It does not protect request bodies or most headers and relies on TLS for confidentiality. RFC 9449 is explicit that TLS is still required.
- "DPoP stops XSS token theft." Malicious script running in the client can ask the browser to sign proofs. DPoP stops exfiltrated tokens being used elsewhere, which is valuable, but injected code acting in place is out of scope. Pair it with Content Security Policy and short lifetimes.
- "The authorisation server did the work." Binding is only enforced where resource servers check it. A gateway that validates the JWT signature but ignores
cnfand the proof gives no protection. See the JWT Security Guide. - "Proofs cannot be replayed." Without nonces, an attacker with temporary key access can pre-generate proofs with future
iatvalues. Use nonces and avoid long-lived DPoP-bound tokens where that risk applies. - "A DPoP response is always bound." If the server returns
token_type: Bearerthe token is not bound; clients should not silently carry on.
Related concepts
- mTLS certificate-bound tokens (RFC 8705): the transport-layer sibling of DPoP. See the mTLS Guide.
- PKCE and PAR: protect the authorisation code;
dpop_jktextends that protection to the token. FAPI 2.0 requires PKCE with S256 and PAR. - Token exchange: when an agent or service exchanges a token downstream, the new token can be sender-constrained too. See the OAuth Token Exchange and Delegation Guide.
- Token lifetime and session design: DPoP lets you keep tokens short without extra round trips per call. See the Token Lifetime Policy Design Guide and the Token and Session Security Guide.
- API keys: static keys have no binding at all; moving to OAuth with DPoP is the stronger pattern. See the API Keys vs OAuth Tokens Guide.
- Federation assurance: NIST SP 800-63C-4 FAL3 needs holder-of-key or bound authenticators, the same proof-of-possession principle applied to assertions. See the SAML vs OIDC Guide.
Practitioner checklist
- Inventory OAuth clients and APIs and mark which tokens are still pure Bearer.
- Confirm the authorisation server publishes
dpop_signing_alg_values_supportedand supportscnf.jktin introspection. - Update resource servers to perform every RFC 9449 Section 4.3 and Section 7 check, including
athand key thumbprint. - Reject any DPoP-bound token presented under the Bearer scheme, and alert on it.
- Generate DPoP keys as non-extractable or hardware-backed keys; never store private keys in browser storage.
- Enable
dpop_bound_access_tokensper client after a pilot, and usedpop_jktwith PKCE or PAR. - Turn on server nonces for high-value APIs and shorten access token lifetimes once binding is enforced.
- Expose
DPoP-NonceandWWW-Authenticatevia CORS for browser clients. - Track the metrics above and set a date to stop accepting Bearer for each API.
Standards and references
- IETF: RFC 9449 OAuth 2.0 Demonstrating Proof of Possession (DPoP)
- IETF: RFC 8705 OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
- IETF: RFC 9700 Best Current Practice for OAuth 2.0 Security
- OpenID Foundation: FAPI 2.0 Security Profile
- NIST: SP 800-63C-4 Federation and Assertions
- NIST: SP 800-53 Rev. 5
- MITRE ATT&CK: Steal Application Access Token
- MITRE ATT&CK: Application Access Token
Related NHI Mgmt Group resources: OAuth 2.0 and OpenID Connect Guide · mTLS Guide · OAuth Token Theft Incident Playbook · API Keys vs OAuth Tokens Guide · Machine-to-Machine OAuth (Client Credentials) Guide