Securing applications with DPoP

Guide for securing applications with DPoP using Keycloak

Standard OAuth 2.0 access tokens are typically Bearer tokens. Any party in possession of the token can use it to access protected resources, regardless of whether they are the legitimate client. If a Bearer token is leaked (e.g., via server logs, network interception, or browser storage), it is vulnerable to unauthorized reuse, known as a replay attack.

OAuth 2.0 Demonstrating Proof-of-Possession (DPoP), defined in RFC 9449, mitigates this risk by making tokens Sender-Constrained.

DPoP binds the access and refresh token to a public/private key pair generated by the client. When accessing a resource, the client must prove it holds the private key corresponding to the token. Consequently, even if an attacker steals a DPoP-bound access token, it cannot be used without the client’s private key.

This guide explains the mechanics of DPoP and how to use it with Keycloak.

When to use DPoP

While DPoP offers superior security, it adds complexity to the client implementation. It is best used in environments where the risk of token leakage is high or where security compliance is strict.

Typical use cases include:

  • Public clients: Single Page Applications (SPAs) or native mobile apps where client secrets cannot be securely stored.

  • Browser-based applications: Where tokens might be exposed via XSS, browser storage, or malicious extensions.

  • High-security environments: Where preventing token replay is a strict compliance requirement (e.g., Financial Grade API).

  • Avoid chaining of services: Application authenticated with Keycloak invokes the REST service service1 with the access token. The service1 should be able to consume the access token, but it should not be able to use that token to invoke further services on behalf of the original application.

DPoP Concepts

At its core, DPoP uses asymmetric cryptography to change the security model from "Bearer" (whoever holds the token has access) to "Sender-Constrained" (only the legitimate owner of the token has access).

The mechanism relies on two fundamental steps:

  • Key Binding: When the client authenticates to receive an access token, it generates a public/private key pair and shares the public key with Keycloak. Keycloak computes a cryptographic fingerprint (thumbprint) of this public key and embeds it directly into the issued access token. The token is now mathematically bound to that specific key pair.

  • Proof of Possession: Whenever the client uses the token to access a protected resource, it must cryptographically sign the request using its private key. The Resource Server checks this signature against the fingerprint embedded in the access token. If the signature is valid, it proves the requestor holds the private key and is therefore the legitimate owner of the token.

DPoP Proof

To technically implement the Proof of Possession mechanism described above DPoP introduces the DPoP proof, a JWT created and signed by the client and sent in the DPoP HTTP header. For every HTTP request, the client must generate a new, unique JWT DPoP Proof.

This proof serves two purposes: it proves ownership of a public key, and it binds the request to a specific URL and HTTP method to prevent replay attacks.

The DPoP Proof Header

The Header of the DPoP proof JWT must contain the public key itself.

  • typ: MUST be dpop+jwt.

  • alg: An asymmetric digital signature algorithm (e.g., ES256, RS256).

  • jwk: The public key chosen by the client in JSON Web Key (JWK) format.

DPoP Proof Header Example
{
  "typ": "dpop+jwt",
  "alg": "ES256",
  "jwk": {
    "kty": "EC",
    "crv": "P-256",
    "x": "f83OJ3D2xF4...",
    "y": "x_FEzRu9Yq8..."
  }
}

The DPoP Proof Body

The Body binds the proof to the specific HTTP request to prevent replay attacks.

  • jti: A unique identifier for the proof.

  • htm: The HTTP method (e.g., POST, GET).

  • htu: The HTTP target URI without query and fragment parts.

  • iat: Creation timestamp.

  • ath: Access Token Hash. Required when accessing resources. It is the base64url encoded SHA-256 hash of the access token.

  • nonce: Included only if the server explicitly requests it via a DPoP-Nonce header.

DPoP Proof Body Example
{
  "jti": "BwC3ESc6acc2lTc",
  "htm": "POST",
  "htu": "https://keycloak.org/realms/test/protocol/openid-connect/token",
  "iat": 1562262616
}

The DPoP flow

Implementing DPoP changes the interaction between the client application and Keycloak. The following section and diagram illustrate the end-to-end lifecycle of a DPoP-bound session.

DPoP flow
Figure 1. DPoP flow

1. Token Binding

When the client sends a token request to Keycloak, it must first generate a key pair. It keeps the private key secure, generates the DPoP proof and sends the key inside a DPoP proof JWT header during the token request.

Keycloak validates the proof and issues an access token. Keycloak takes the public key from the proof, calculates its "thumbprint" (base64url encoded SHA-256 hash of the JWK), and embeds it directly into the access token in a claim called cnf (confirmation).

POST /realms/myrealm/protocol/openid-connect/token HTTP/1.1
DPoP: <DPoP-Proof-JWT>
...

The resulting JWT access token now carries the SHA-256 thumbprint of the client’s key:

{
  "iss": "https://keycloak.org/realms/test",
  "token_type": "DPoP",
  "cnf": {
    "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
  }
}

2. Resource Access

When the client calls a protected API, it must send two things: the access token (in the Authorization header) and a new DPoP proof (in the DPoP header).

The Resource Server performs a check that standard Bearer validation does not: it compares the key inside the DPoP proof with the thumbprint inside the access token. If they don’t match, or if the proof is missing, access is denied.

GET /protected-resource HTTP/1.1
Authorization: DPoP <The-Access-Token>
DPoP: <New-DPoP-Proof>
Note that when using DPoP-bound tokens, the Authorization scheme is DPoP, not Bearer.

The Nonce Mechanism

To provide stricter protection against replay attacks, DPoP supports a challenge-response mechanism using a nonce.

If a Resource Server requires a nonce (or if the provided nonce is too old), it rejects the request with a 401 Unauthorized status and includes a DPoP-Nonce HTTP header containing a new value. The client must then generate a new DPoP proof that includes this value in the nonce claim and retry the request. This ensures that a proof captured by an attacker cannot be replayed effectively, as the server will invalidate the underlying nonce after use or a short timeout.

Configuring Keycloak

To force a client to use DPoP, you need to configure the client in the Admin Console.

Navigate to your client’s Capability config in the Settings section and locate the Require DPoP bound tokens switch.

  • If enabled: The client must send a valid DPoP proof. All token requests must include a valid DPoP proof. If a client tries to send a token request without a valid DPoP Proof, the request will fail.

  • If disabled: The client may send a DPoP proof. If the client sends a proof, Keycloak will bind the token. If not, it issues a standard Bearer token.

Note that this Keycloak switch maps to the dpop_bound_access_tokens client registration metadata defined by the DPoP specification.

Handling Refresh Tokens

The behavior for refresh tokens differs slightly depending on the client type:

  • Public Clients: Since these clients cannot hold secrets safely, DPoP is critical. Both the access token and the refresh token are bound to the key. The client must send the DPoP proof signed with the same private key also for the refresh request.

  • Confidential Clients: These clients are already authenticated for example with Client ID and Secret. Therefore, Keycloak binds only the access token to the DPoP key, relying on the client credentials to secure the refresh token.

Other Keycloak Endpoints

When Keycloak acts as a Resource Server, it strictly enforces DPoP checks on the following endpoints if a DPoP-bound token is used:

  • UserInfo Endpoint: Requires a valid DPoP proof matching the access token.

  • Logout Endpoint: For public clients, the logout request (using a refresh token) requires a DPoP proof.

  • Admin & Account APIs: Any request to Keycloak’s internal REST APIs using a DPoP token must include a valid proof.

Client Policies

For granular control, you can use the dpop-bind-enforcer executor within Client Policies. This is useful for advanced scenarios:

  • Auto-Configuration: Automatically enable DPoP requirements for all newly registered clients.

  • Refresh Token Only: Enforce DPoP binding only for the Refresh Token while keeping the access token as a standard Bearer token. This increases security for public clients while maintaining compatibility with legacy resource servers that do not support DPoP.

  • Strict OIDC Enforcement: Require clients to send the dpop_jkt parameter during the initial Authorization Code flow, binding the entire flow.

On this page