keycloak/docs/guides/securing-apps/dpop.adoc
Giuseppe Graziano 46d1c4fa5a Sender constrained tokens for token exchange
Closes #46092

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
2026-04-01 10:23:51 +02:00

190 lines
10 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<#import "/templates/guide.adoc" as tmpl>
<#import "/templates/links.adoc" as links>
<@tmpl.guide
title="Securing applications with Demonstrating Proof-of-Possession (DPoP)"
priority=150
summary="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 https://datatracker.ietf.org/doc/html/rfc9449[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 clients private key.
This guide explains the mechanics of DPoP and how to use it with {project_name}.
== 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 {project_name}. {project_name} 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
[source,json]
----
{
"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
[source,json]
----
{
"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 {project_name}. The following section and diagram illustrate the end-to-end lifecycle of a DPoP-bound session.
.DPoP flow
image::dpop-flow.dio.svg[DPoP flow]
=== 1. Token Binding
When the client sends a token request to {project_name}, 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.
{project_name} validates the proof and issues an access token. {project_name} 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).
[source,http]
----
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:
[source,json]
----
{
"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.
[source,http]
----
GET /protected-resource HTTP/1.1
Authorization: DPoP <The-Access-Token>
DPoP: <New-DPoP-Proof>
----
NOTE: 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 {project_name}
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, {project_name} will bind the token. If not, it issues a standard Bearer token.
Note that this {project_name} 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, {project_name} binds only the access token to the DPoP key, relying on the client credentials to secure the refresh token.
=== Other {project_name} Endpoints
When {project_name} 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 {project_name}'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.
== DPoP with Standard Token Exchange
* Standard Token Exchange does not support DPoP-bound tokens (as defined in RFC 7800) as the `subject_token` parameter. Only Bearer access tokens can be exchanged. If you attempt to use a DPoP-bound token as the subject token, the request will be rejected with an `invalid_request` error.
* While DPoP-bound tokens cannot be used as `subject_token` to token exchange, you can obtain DPoP-bound tokens as **output**.
=== When to Use Token Exchange with DPoP
Token exchange with DPoP is useful when:
* You need to upgrade a Bearer token to a DPoP-bound token for increased security
* You want to down-scope or change audiences while simultaneously adding DPoP binding
* Your backend services require DPoP but the original token was issued as Bearer
</@tmpl.guide>