Navigation

Loading...
Loading theme...
Introduction
Step 0 of 8
0. Introduction
1. User Login
2. Authorization Server
3. Authorization Code
4. Key Generation
5. DPoP Proof Creation
6. Token Exchange
7. API Request
8. Security Demo

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.

Read RFC 9449

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.

https://app.example.com

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_uriquery 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=codeIndicates authorization code grant flow
client_idUnique identifier for the requesting application
redirect_uriURL where the user will be redirected after authentication
scopePermissions requested (openid, profile, email)
stateRandom value for CSRF protection
code_challengeSHA256 hash of code verifier (PKCE security)
dpop_jktJSON Web Key thumbprint for DPoP token binding

Handle Authorization Response

After successful authentication, the authorization server will redirect back to your application with an authorization code that can be exchanged for tokens.

https://auth.example.com/oauth/authorize?response_type=code&client_id=myapp-client-id&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid+profile+email&state=abc123xyz&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&dpop_jkt=CNx2APrOJh-w5OoRFRSD6WoeBJJUgZ-oUr_p7SPSQgQ

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.

https://app.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=abc123xyz
S
Samsam@example.com

Private Key (JWK Format)

No keys generated

Public Key (JWK Format)

No keys generated

Public Key JWK Thumbprint (RFC 7638)

Public key thumbprint will appear after key generation

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 identifier
alg: ES256signature algorithm
jwk: Public key for verification

The DPoP Proof payload

jti: Unique JWT identifier
htm: HTTP method (POST)
htu: Target token endpoint
iat: Issued at timestamp

Security 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
Header
Payload
Signature

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
/token
HTTP/1.1
Host:
auth.example.com
Content-Type:
application/x-www-form-urlencoded
DPoP:
loading.loading.loading
grant_type=authorization_code
code=SplxlOBeZQQYbYS6WxSbIA
redirect_uri=https://app.example.com/callback
client_id=myapp-client-id

2. DPoP-Protected Access Token (JWT)

eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6ImF1dGgtc2VydmVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJzYW1AZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImV4cCI6MTc1NzMyODk0MiwiaWF0IjoxNzU3MzI1MzQyLCJjbmYiOnsiamt0IjoiTnpiTHNYaDh1RENjZC02TU53WEY0V183bm9XWEZaQWZIa3hac1JHQzlYcyJ9LCJzY29wZSI6InJlYWQgd3JpdGUiLCJlbWFpbCI6InNhbUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlfQ.bW9ja19zaWduYXR1cmVfenF5ZjFzYXhkaV9tZmF5NHdqZA
Header
Payload (with jkt)
Signature

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_tokengrant 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 Authorizationheader, 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.1
Host:
api.example.com
Authorization:
DPoP
eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6ImF1dGgtc2VydmVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJzYW1AZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImV4cCI6MTc1NzMyODk0MiwiaWF0IjoxNzU3MzI1MzQyLCJjbmYiOnsiamt0IjoiTnpiTHNYaDh1RENjZC02TU53WEY0V183bm9XWEZaQWZIa3hac1JHQzlYcyJ9LCJzY29wZSI6InJlYWQgd3JpdGUiLCJlbWFpbCI6InNhbUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlfQ.bW9ja19zaWduYXR1cmVfMndtazNhX3NhbQ
DPoP:
eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiV0tuLVpJR2V2Y3dHSXl5cnpGb1pOQmRhcTlfVHNxekdId0hpdEpCY0tXUGlMY244X2pIWHk5dTRncCIsInkiOiJ5NzdhczVXWVVMR2pyTFVLVl9EOFhtY01EZEN5RUZCQ1pWZ1pLVkVqTmZVX3FnUlpVZ0xVVjV3UVZCYSIsImFsZyI6IkVTMjU2In19.eyJqdGkiOiJkMmYwNWQ4LTg3MTMtMTFlYi1hOTM1LTAyNDJhYzExMDAwMyIsImh0bSI6IkdFVCIsImh0dSI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tL3Byb2ZpbGUiLCJpYXQiOjE3NTczMjUzNDIsImF0aCI6ImRCamZ0SmVaNENWUC1tQjkySzI3dWhiVUpVMXAxcl93VzFnRldGT0VqWGsifQ.api_signature_example_here
Accept:
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.1
Host:
api.example.com
Authorization:
Bearer
eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6ImF1dGgtc2VydmVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJzYW1AZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImV4cCI6MTc1NzMyODk0MiwiaWF0IjoxNzU3MzI1MzQyLCJjbmYiOnsiamt0IjoiTnpiTHNYaDh1RENjZC02TU53WEY0V183bm9XWEZaQWZIa3hac1JHQzlYcyJ9LCJzY29wZSI6InJlYWQgd3JpdGUiLCJlbWFpbCI6InNhbUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlfQ.bW9ja19zaWduYXR1cmVfMndtazNhX3NhbQ
Accept:
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.1
Host:
api.example.com
Authorization:
DPoP
eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6ImF1dGgtc2VydmVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJzYW1AZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImV4cCI6MTc1NzMyODk0MiwiaWF0IjoxNzU3MzI1MzQyLCJjbmYiOnsiamt0IjoiTnpiTHNYaDh1RENjZC02TU53WEY0V183bm9XWEZaQWZIa3hac1JHQzlYcyJ9LCJzY29wZSI6InJlYWQgd3JpdGUiLCJlbWFpbCI6InNhbUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlfQ.bW9ja19zaWduYXR1cmVfMndtazNhX3NhbQ
DPoP:
eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiZjgzT0ozRDJ4RjFCZzh2dWI5dExlMWdITXpWNzZlOFR1czl1UEh2UlZFVSIsInkiOiJ4X0ZFelJ1OW0zNkhMTl90dWU2NTlMTnBYVzZwQ3lTdGlrWWpLSVdJNWEwIiwiYWxnIjoiRVMyNTYifX0.eyJqdGkiOiJtYWxpY2lvdXMtYXR0ZW1wdC0xMjMiLCJodG0iOiJHRVQiLCJodHUiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbS9wcm9maWxlIiwiaWF0IjoxNzU3MzI1MzQyLCJhdGgiOiJkQmpmdEplWjRDVlAtbUI5MksyN3VoYlVKVTFwMXJfd1cxZ0ZXRk9FalhrIn0.FAKE_MALICIOUS_SIGNATURE_WILL_NOT_VERIFY
Accept:
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.