DPoP
DPoP (or Demonstration of Proof-of-Possession at the Application Layer) is an application-level mechanism for sender-constraining OAuth tokens and refresh tokens as specified in RFC 9449. A method is needed to prove the possession of a private/public key pair by including a DPoP header on an HTTP request.
The value of the header is a DPoP Proof JWT (RFC 7519) that enables the authorization server or API as well clients to bind the auth and request flow with a client's key and some claims about the current HTTP request. It provides a mechanism to bind the authentication process into each other by linking the corresponding process of the DPoP proof.
1. A user want to login on your application
Most applications require users to login to see certain pages or perform specific actions. In a modern OAuth 2.0/OpenID Connect scenario, the user is usually redirected to a the authorization server to perform the authentication.
When the user clicks the login button, we initiate a redirect to the authorization server and request an Authorization Code using the OAuth 2.0 Authorization Code Flow with PKCE.
What happens in this step:
- User arrives at your application
- Application detects unauthenticated state
- Login button triggers OAuth flow initiation
- PKCE parameters are generated for security
Redirect to Authorization Server
Now let's see how the application redirects to the authorization server with all the necessary OAuth 2.0 parameters, including DPoP-specific headers for token binding.
For this example we're using the Authorization Code Grant. Depending on your type of application this might not be the right choice. How to leverage DPoP to demonstrate proof of possession will remain the same for all OAuth flows.
2. Redirect to Authorization Server
Once the user has clicked on the login button within our application, we will redirect to our Authorization Server to deal with logging in the user.
If the user provides the correct credentials, the Authorization server will redirect back to the application as specified in the redirect_uri
query parameter. It will also attach an Authorization code, which we exchange for an Access Token.
In this step we'll perform:
- Redirect includes OAuth 2.0 parameters
- DPoP header sent with authorization request
- User authenticates with credentials
- Authorization server validates identity
Authorization Request Parameters
response_type=code
— Indicates authorization code grant flowclient_id
— Unique identifier for the requesting applicationredirect_uri
— URL where the user will be redirected after authenticationscope
— Permissions requested (openid, profile, email)state
— Random value for CSRF protectioncode_challenge
— SHA256 hash of code verifier (PKCE security)dpop_jkt
— JSON Web Key thumbprint for DPoP token bindingHandle Authorization Response
After successful authentication, the authorization server will redirect back to your application with an authorization code that can be exchanged for tokens.
Sign in to your account
This app requires you to sign in to your account.
3. Authorization Code Received
After successful authentication, the authorization server redirects back to your application with an authorization code.
The user is now logged in and can access the application dashboard. The authorization code (SplxlOBeZQQYbYS6WxSbIA
) will be exchanged for DPoP-bound tokens in the next step.
Generate Cryptographic Keys
Before exchanging the authorization code for tokens, we need to generate a cryptographic key pair that will be used to create DPoP proofs and bind tokens to this specific client.
The authorization code (e.g., "SplxlOBeZQQYbYS6WxSbIA") is short-lived and will be exchanged for DPoP-bound access tokens, providing cryptographic proof of key possession.
Private Key (JWK Format)
Public Key (JWK Format)
Public Key JWK Thumbprint (RFC 7638)
Step 4: Generate Key Pair
Before creating DPoP proofs, we need to generate a cryptographic key pair. The public key will be included in DPoP JWTs, while the private key will be used for signing.
Key Generation Process
The client generates a key pair using the configured algorithm (ES256). This key pair will be used for all subsequent DPoP proofs.
The public key is formatted as a JSON Web Key (JWK) and will be included in the DPoP JWT header for server verification.
Public Key JWK Thumbprint
The public key JWK thumbprint is a SHA-256 hash of the canonical public key JWK representation (RFC 7638). It provides a stable identifier for the key pair.
This thumbprint can be used by authorization servers to track and bind tokens to specific key pairs across multiple requests without needing to store the full JWK.
Security Benefits
- Fresh key pair generated for each session
- Private key never leaves the client
- Public key enables server verification
- Thumbprint provides stable key identification
Create Your First DPoP Proof
With your key pair generated, you're ready to create a DPoP proof JWT. This proof will demonstrate possession of the private key and can be used to bind tokens to your client application.
Step 5: Generate DPoP Proof JWT
DPoP introduces the concept of a DPoP proof, which is a JSON Web Token (JWT) created by the client and sent with an HTTP request using the DPoP header field. Each HTTP request requires a unique DPoP proof
How It Works
A valid DPoP proof demonstrates to the server that the client holds the private key that was used to sign the DPoP proof JWT. This enables authorization servers to bind issued tokens to the corresponding public key and for resource servers to verify the key-binding of tokens that it receives, which prevents said tokens from being used by any entity that does not have access to the private key.
The DPoP proof demonstrates possession of a key and, by itself, is not an authentication or access control mechanism.
The DPoP Proof header
typ
: DPoP JWT type identifieralg
: ES256signature algorithmjwk
: Public key for verificationThe DPoP Proof payload
jti
: Unique JWT identifierhtm
: HTTP method (POST)htu
: Target token endpointiat
: Issued at timestampSecurity Benefits
- Cryptographically signed with the client's private key
- Bound to specific HTTP method and target URL
- Contains the public key for server verification
- Prevents token theft and replay attacks
DPoP Proof Ready!
The DPoP proof JWT is now ready to be used in the token exchange request. This proof will bind the access token to your cryptographic key pair.
Complete DPoP Proof JWT
loading.loading.loading
This complete JWT is sent as the DPoP
header value
Decoded JWT Header
{ "typ": "dpop+jwt", "alg": "ES256", "jwk": { "kty": "EC", "crv": "P-256", "x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGHwHitJBcKWPiLcn8_jHXy9u4gp", "y": "y77as5WYULGjrLUKV_D8XmcMDdCyEFBCZVgZKVEjNfU_qgRZUgLUV5wQVBa", "alg": "ES256" } }
Decoded JWT Payload
{ "jti": "c1f05d8-8713-11eb-a935-0242ac110002", "htm": "POST", "htu": "https://auth.example.com/oauth/token", "iat": 1757325342 }
1. Access token Request with DPoP Proof
POST/tokenHTTP/1.1Host:auth.example.comContent-Type:application/x-www-form-urlencodedDPoP:loading.loading.loadinggrant_type=authorization_codecode=SplxlOBeZQQYbYS6WxSbIAredirect_uri=https://app.example.com/callbackclient_id=myapp-client-id
2. DPoP-Protected Access Token (JWT)
eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6ImF1dGgtc2VydmVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJzYW1AZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImV4cCI6MTc1NzMyODk0MiwiaWF0IjoxNzU3MzI1MzQyLCJjbmYiOnsiamt0IjoiTnpiTHNYaDh1RENjZC02TU53WEY0V183bm9XWEZaQWZIa3hac1JHQzlYcyJ9LCJzY29wZSI6InJlYWQgd3JpdGUiLCJlbWFpbCI6InNhbUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlfQ.bW9ja19zaWduYXR1cmVfenF5ZjFzYXhkaV9tZmF5NHdqZA
3. Decoded Access Token Payload
{ "iss": "https://auth.example.com", "sub": "sam@example.com", "aud": "https://api.example.com", "exp": 1757328942, "iat": 1757325342, "cnf": { "jkt": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs" }, "scope": "read write", "email": "sam@example.com", "email_verified": true }
Note the cnf.jkt
claim containing the DPoP public key thumbprint
Step 6: Exchange Authorization Code
Exchange the authorization code for an access token with the DPoP proof.
Attach the DPoP proof to the request using the DPoP header
To request an access token that is bound to a public key using DPoP, the client must provide a valid DPoP proof JWT in a DPoP header when making an access token request to the authorization server's /token
endpoint. This is applicable for all access token requests regardless of grant type (including, for example, the common authorization_code
and refresh_token
grant types but also extension grants such as the JWT authorization grant).
Validate DPoP proof on the Authorization server
The Authorization server can validate that the DPoP proof found in the DPoP header has a valid signature, and is issued for the correct https://auth.example.com/token
URI and POST
HTTP method.
Add the DPoP public key thumbprint to the Access Token
When access tokens are represented as JSON Web Tokens, the DPoP proof's public key information should be represented using the jkt
confirmation method member in the access token's header.
To convey the hash of a public key in a JSON Web Token, the specification introduces the JSON Web Key Thumbprint jkt
member under the confirmation cnf
claim.
With the DPoP proof's public key thumbprint available in the Access Token, we can validate subsequent DPoP proofs are issued by the same application that requested the Access Token.
Use OAuth's introspection to confirm the Public Key's thumbprint
If your access token is not a JSON Web Token, you can use OAuth's introspection endpoint to get more metainformation about the token. When you query the introspection endpoint for a DPoP protected Access Token, it should also return that confirmation cnf
claim with the DPoP Proof's public key thumbprint jkt
.
What's Next?
With the DPoP-bound access token, the client can now make secure API requests. Each API call must include a new DPoP proof that matches the token's bound key pair.
Step 7: Request Protected API
Make authenticated requests to protected endpoints using the DPoP-bound access token.
DPoP Authentication Scheme
To make a request to a protected API endpoint using our DPoP protected Access token, we can attach it to the Authorization
header, using the DPoP authentication scheme.
Access Token Hash (ath) Claim
The request should also contain the DPoP header with a new DPoP proof. This DPoP proof must include an Access Token Hash (ath
) claim with a valid hash of the associated Access Token.
Compatibility with Bearer Authentication
Protected resources simultaneously supporting both the DPoP and Bearer schemes need to update how evaluation of bearer tokens is performed to prevent downgraded usage of a DPoP-bound access token.
Specifically, such a protected resource MUST reject a DPoP-bound access token received as a bearer token.
DPoP Implementation Complete!
You've successfully implemented the complete DPoP flow! The access token is cryptographically bound to your key pair and can be used securely for API requests with proof of possession.
Protected API Request with DPoP
GET/profile HTTP/1.1Host:api.example.comAuthorization:DPoPeyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6ImF1dGgtc2VydmVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJzYW1AZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImV4cCI6MTc1NzMyODk0MiwiaWF0IjoxNzU3MzI1MzQyLCJjbmYiOnsiamt0IjoiTnpiTHNYaDh1RENjZC02TU53WEY0V183bm9XWEZaQWZIa3hac1JHQzlYcyJ9LCJzY29wZSI6InJlYWQgd3JpdGUiLCJlbWFpbCI6InNhbUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlfQ.bW9ja19zaWduYXR1cmVfMndtazNhX3NhbQDPoP:eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiV0tuLVpJR2V2Y3dHSXl5cnpGb1pOQmRhcTlfVHNxekdId0hpdEpCY0tXUGlMY244X2pIWHk5dTRncCIsInkiOiJ5NzdhczVXWVVMR2pyTFVLVl9EOFhtY01EZEN5RUZCQ1pWZ1pLVkVqTmZVX3FnUlpVZ0xVVjV3UVZCYSIsImFsZyI6IkVTMjU2In19.eyJqdGkiOiJkMmYwNWQ4LTg3MTMtMTFlYi1hOTM1LTAyNDJhYzExMDAwMyIsImh0bSI6IkdFVCIsImh0dSI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tL3Byb2ZpbGUiLCJpYXQiOjE3NTczMjUzNDIsImF0aCI6ImRCamZ0SmVaNENWUC1tQjkySzI3dWhiVUpVMXAxcl93VzFnRldGT0VqWGsifQ.api_signature_example_hereAccept:application/json
DPoP Proof Payload (with ath claim)
{ "jti": "d2f05d8-8713-11eb-a935-0242ac110003", "htm": "GET", "htu": "https://api.example.com/profile", "iat": 1757325342, "ath": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" }
Note the ath
claim containing the SHA-256 hash of the access token
Attack 1: Bearer Token Downgrade
Malicious Request
GET/profile HTTP/1.1Host:api.example.comAuthorization:BearereyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6ImF1dGgtc2VydmVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJzYW1AZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImV4cCI6MTc1NzMyODk0MiwiaWF0IjoxNzU3MzI1MzQyLCJjbmYiOnsiamt0IjoiTnpiTHNYaDh1RENjZC02TU53WEY0V183bm9XWEZaQWZIa3hac1JHQzlYcyJ9LCJzY29wZSI6InJlYWQgd3JpdGUiLCJlbWFpbCI6InNhbUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlfQ.bW9ja19zaWduYXR1cmVfMndtazNhX3NhbQAccept:application/json
Server Response
HTTP/1.1 400 Bad Request Content-Type: application/json { "error": "invalid_token", "error_description": "DPoP-bound access token cannot be used with Bearer authentication scheme" }
❌ Request rejected - Security violation detected
Attack 2: Forged DPoP Proof
Malicious Request
GET/profile HTTP/1.1Host:api.example.comAuthorization:DPoPeyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6ImF1dGgtc2VydmVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJzYW1AZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImV4cCI6MTc1NzMyODk0MiwiaWF0IjoxNzU3MzI1MzQyLCJjbmYiOnsiamt0IjoiTnpiTHNYaDh1RENjZC02TU53WEY0V183bm9XWEZaQWZIa3hac1JHQzlYcyJ9LCJzY29wZSI6InJlYWQgd3JpdGUiLCJlbWFpbCI6InNhbUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlfQ.bW9ja19zaWduYXR1cmVfMndtazNhX3NhbQDPoP:eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiZjgzT0ozRDJ4RjFCZzh2dWI5dExlMWdITXpWNzZlOFR1czl1UEh2UlZFVSIsInkiOiJ4X0ZFelJ1OW0zNkhMTl90dWU2NTlMTnBYVzZwQ3lTdGlrWWpLSVdJNWEwIiwiYWxnIjoiRVMyNTYifX0.eyJqdGkiOiJtYWxpY2lvdXMtYXR0ZW1wdC0xMjMiLCJodG0iOiJHRVQiLCJodHUiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbS9wcm9maWxlIiwiaWF0IjoxNzU3MzI1MzQyLCJhdGgiOiJkQmpmdEplWjRDVlAtbUI5MksyN3VoYlVKVTFwMXJfd1cxZ0ZXRk9FalhrIn0.FAKE_MALICIOUS_SIGNATURE_WILL_NOT_VERIFYAccept:application/json
Server Response
HTTP/1.1 401 Unauthorized Content-Type: application/json { "error": "invalid_dpop_proof", "error_description": "DPoP proof signature verification failed - public key mismatch" }
❌ Request rejected - Security violation detected
DPoP Security Benefits
See how DPoP protects against common attack scenarios where malicious actors attempt to misuse stolen access tokens.
Attack 1: Bearer Token Downgrade
A malicious actor attempts to use the DPoP-bound access token as a regular Bearer token. The API server detects this downgrade attack and rejects the request.
Attack 2: Forged DPoP Proof
An attacker tries to create their own DPoP proof using a different key pair. The API server validates the proof signature against the bound key and rejects the mismatched proof.
DPoP Protection
- Prevents token theft and replay attacks
- Cryptographically binds tokens to key pairs
- Detects downgrade to Bearer authentication
- Validates proof signatures on every request
DPoP Successfully Prevents Both Attacks!
Your access token is cryptographically bound to your private key, making it useless to attackers who don't possess the corresponding private key used during token issuance.