Demonstrating Proof-of-Possession (DPoP)

On this page
Enterprise plan required

This feature requires an Enterprise plan.

Available since 1.63.0

FusionAuth issues bearer tokens by default. Anyone who holds a bearer token can use the token. Demonstrating Proof-of-Possession (DPoP) binds a token to a key pair that your client generates. To use a bound token, the client signs a short-lived JWT called a DPoP proof and sends the proof with every request. An attacker who steals a bound token can't use the token without the private key.

FusionAuth implements DPoP as defined in RFC 9449. FusionAuth acts as the authorization server: FusionAuth checks the proof on token requests and issues bound tokens. Your API acts as the resource server and checks the proof on each request.

To add DPoP to a single-page app, see Use DPoP In A Browser App. To check DPoP proofs in your API, see Validate DPoP Requests In Your API.

How DPoP Works#

The client creates a key pair before the first token request. Each request then carries a new proof signed with the private key. The proof names the HTTP method and URL of the request, so a proof captured from one request doesn't work for another. Requests that carry an access token also include a hash of that access token in the proof.

APIFusionAuthClientAPIFusionAuthClientGenerate a key pairVerify the proofCompute the key thumbprintVerify the proofMatch the key to cnf.jktToken request with a DPoP proofAccess token with cnf.jkttoken_type DPoPAuthorization: DPoP <access token>DPoP: <new proof>Response

The client binds tokens to a key at the token endpoint, then proves possession of the key on each API request.

FusionAuth checks the proof signature against the public key in the proof header. FusionAuth then computes the JWK SHA-256 thumbprint of the public key and writes the thumbprint into the access token. Your API compares the thumbprint in the access token with the key in each new proof. A request signed with a different key fails.

Turn On DPoP#

FusionAuth has no DPoP setting. A client opts in by sending a DPoP header on a token request. When the request carries a valid proof, FusionAuth binds the tokens. When the request has no proof, FusionAuth issues bearer tokens.

You can't require DPoP for an Application. Without an Enterprise license, a token request that includes a proof fails with a not_licensed error.

What FusionAuth Binds#

When a token request includes a valid proof, FusionAuth binds the tokens in the response:

  • The access token gets a cnf claim. The jkt value is the thumbprint of the public key in the proof.
  • The token response returns DPoP as the token_type.
  • FusionAuth binds the refresh token to the same thumbprint. FusionAuth stores the binding with the refresh token, because a refresh token isn't a JWT.

FusionAuth doesn't bind Id tokens.

A bound access token contains a cnf claim like the following:

{
  "cnf": {
    "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
  }
}

The Introspect endpoint also returns the cnf claim for bound tokens.

Bind The Authorization Code#

By default, FusionAuth binds tokens at the token request. To bind the authorization code too, send the thumbprint of your public key in the dpop_jkt parameter of the Authorize endpoint. FusionAuth then requires a proof from the same key when the client exchanges the code.

Supported Grants#

FusionAuth accepts DPoP proofs on the /oauth2/token endpoint for the following grants:

  • Authorization code
  • Refresh token
  • Client credentials
  • Password
  • Device code

For the device code grant, send the proof to /oauth2/device_authorize. FusionAuth binds the device code to the key, and the token request must carry a proof from the same key. A token request with a proof for a device code issued without a proof fails. Support for DPoP with the device code grant follows a draft specification.

The token exchange grant doesn't bind tokens. The token exchange grant also rejects a bound subject token. See Token Exchange.

Where FusionAuth Accepts A Bound Token#

FusionAuth accepts a bound access token with the DPoP scheme and a new proof in the following places:

FusionAuth rejects a bound access token sent any other way, including with the Bearer or JWT scheme or in the access_token cookie. The rejection stops an attacker from downgrading a stolen bound token to a bearer token.

FusionAuth applies the same rule to refresh tokens. A bound refresh token requires a proof from the same key. An unbound refresh token rejects any proof, so a client can't bind a bearer session to a new key.

Proof Requirements#

FusionAuth accepts proofs signed with the following algorithms: Ed448, Ed25519, ES256, ES384, ES512, PS256, PS384, PS512, RS256, RS384, and RS512. The OpenID Connect discovery document lists the same algorithms in dpop_signing_alg_values_supported.

FusionAuth accepts a proof for 10 seconds after the proof's iat time, plus 15 seconds of clock skew in either direction. For every claim a proof contains, see Proof Claims.

Limitations#

FusionAuth doesn't support DPoP nonces. FusionAuth never returns a DPoP-Nonce header, and FusionAuth ignores the nonce claim.

FusionAuth checks that each proof has a jti claim, but FusionAuth doesn't record jti values. FusionAuth relies on the short proof lifetime to limit replay. To reject replayed proofs at your API, store jti values in your API. See Validate DPoP Requests In Your API.

DPoP protects a token after theft. DPoP doesn't protect against code running in the user's browser, because that code can ask the client to sign proofs. To compare DPoP with other ways to store tokens in a browser, see Token Storage.