What is the DPoP header?
The DPoP request header, defined in Section 4.1 of RFC 9449 (OAuth 2.0 Demonstrating Proof of Possession, Standards Track, September 2023), carries a DPoP proof: a JWT the client signs with a private key it holds, on every request where it uses or obtains a DPoP-bound token. The authorization server binds the issued access token to the matching public key, so a stolen token is useless without the key.
The value is a single JWT in token68 syntax. Its JOSE header MUST include typ set to dpop+jwt, an asymmetric alg (never none and never a MAC algorithm), and jwk, the public key. Its payload MUST include:
| Claim | Meaning |
|---|---|
jti |
Unique identifier for this proof |
htm |
HTTP method of the request |
htu |
Target URI, without query or fragment |
iat |
Creation time |
ath |
Base64url SHA-256 hash of the access token, when one is presented |
nonce |
The server’s latest DPoP-Nonce, when one was provided |
At the token endpoint, the proof asks for a bound token, and the response says "token_type": "DPoP". At a resource server, the token goes in Authorization with the DPoP scheme, not Bearer, and the proof goes in this header. See OAuth 2.0 and token lifecycle.
API Usage & Best Practices
- Check everything Section 4.3 lists. Exactly one
DPoPheader; a well-formed JWT;typisdpop+jwt;algis asymmetric and acceptable; the signature verifies with the embeddedjwk;jwkhas no private key;htmandhtumatch this request;noncematches if you issued one;iatis recent; and at a resource server,athmatches the token and the key matches the one the token is bound to. Skipping the binding check turns DPoP back into bearer tokens. - Accept proofs only briefly, and track
jti. RFC 9449 says servers MUST only accept proofs for a limited time after creation, preferably seconds or minutes. Rememberingjtivalues for that window blocks replay outright, where your servers share state. - Normalize before comparing
htu. The RFC says servers SHOULD apply syntax-based and scheme-based URI normalization first. Behind a proxy, compare against the URI the client used, not the internal one. - Reject a bound token sent as
Bearer. A resource server that supports both schemes MUST refuse a DPoP-bound token presented withBearer, or a thief can skip the proof by downgrading. - Make a new proof for every request, including retries. A proof is single-use where
jtiis tracked, so replaying the same request with the same proof can fail. RFC 9449 recommends a fresh proof even when retrying idempotent requests. - Challenge with
WWW-Authenticate: DPoP. Return401 UnauthorizedwithWWW-Authenticate: DPoPand analgsparameter listing acceptable algorithms. Useerror="invalid_dpop_proof"when the proof fails checks, anderror="use_dpop_nonce"with aDPoP-Noncewhen you require a nonce. - Expose headers to browser clients. Single-page apps can only read
WWW-AuthenticateandDPoP-Nonceacross origins if you list them inAccess-Control-Expose-Headers. - It is origin authentication only. The
DPoPscheme MUST NOT be used withProxy-AuthenticateorProxy-Authorization. - Prefer it to bearer tokens where clients can hold a key. DPoP works at the application layer, so it fits public clients such as browser and mobile apps that cannot do mutual TLS. Where mTLS is available end to end, certificate-bound tokens (RFC 8705) are the alternative.
Examples
A token request carrying a DPoP proof, from RFC 9449 Figure 5:
POST /token HTTP/1.1Host: server.example.comContent-Type: application/x-www-form-urlencodedDPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwieCI6Imw4dEZyaHgtMzR0VjNoUklDUkRZOXpDa0RscEJoRjQyVVFVZldWQVdCRnMiLCJ5IjoiOVZFNGpmX09rX282NHpiVFRsY3VOSmFqSG10NnY5VERWclUwQ2R2R1JEQSIsImNydiI6IlAtMjU2In19.eyJqdGkiOiItQndDM0VTYzZhY2MybFRjIiwiaHRtIjoiUE9TVCIsImh0dSI6Imh0dHBzOi8vc2VydmVyLmV4YW1wbGUuY29tL3Rva2VuIiwiaWF0IjoxNTYyMjYyNjE2fQ.2-GxA6T8lP4vfrg8v-FdWP0A0zdrj8igiMLvqRMUvwnQg4PtFLbdLXiOSsX0x7NVY-FNyJK70nfbV37xRZT3Lg
grant_type=authorization_code&client_id=s6BhdRkqt&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb&code_verifier=bEaL42izcC-o-xBk0K2vuJ6U-y1p9r_wW2dFWIWgjz-Using the bound token at a resource server, from RFC 9449 Figure 13. The proof’s ath claim is the hash of the token in Authorization:
GET /protectedresource HTTP/1.1Host: resource.example.orgAuthorization: DPoP Kz~8mXK1EalYznwH-LC-1fBAo.4Ljp~zsPE_NeO.gxUDPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwieCI6Imw4dEZyaHgtMzR0VjNoUklDUkRZOXpDa0RscEJoRjQyVVFVZldWQVdCRnMiLCJ5IjoiOVZFNGpmX09rX282NHpiVFRsY3VOSmFqSG10NnY5VERWclUwQ2R2R1JEQSIsImNydiI6IlAtMjU2In19.eyJqdGkiOiJlMWozVl9iS2ljOC1MQUVCIiwiaHRtIjoiR0VUIiwiaHR1IjoiaHR0cHM6Ly9yZXNvdXJjZS5leGFtcGxlLm9yZy9wcm90ZWN0ZWRyZXNvdXJjZSIsImlhdCI6MTU2MjI2MjYxOCwiYXRoIjoiZlVIeU8ycjJaM0RaNTNFc05yV0JiMHhXWG9hTnk1OUlpS0NBcWtzbVFFbyJ9.2oW9RP35yRqzhrtNP86L-Ey71EOptxRimPPToA1plemAgR6pxHF8y6-yqyVnmcw6Fy1dqd-jfxSYoMxhAJpLjAA resource server rejecting a token whose key binding does not match:
HTTP/1.1 401 UnauthorizedWWW-Authenticate: DPoP error="invalid_token", error_description="Invalid DPoP key binding", algs="ES256"