---
id: 6
title: "Bind tokens to a key: sender-constrained tokens (DPoP)"
status: current
date: 2026-06-12
authors:
  - "Theo Zourzouvillys"
tags: [security, auth, http]
summary: "A bearer token grants access to whoever holds it — steal it, replay it. Bind the token to a holder key (DPoP, RFC 9449) so using it requires proving possession of a private key the token names. A stolen token alone becomes useless."
supersedes: null
superseded_by: null
aliases: []
references:
  - id: rfc9449
    title: "RFC 9449 — OAuth 2.0 Demonstrating Proof of Possession (DPoP)"
    url: https://www.rfc-editor.org/rfc/rfc9449
    abstract: "Defines DPoP: sender-constraining OAuth tokens by binding them to a public key the client holds. Each request carries a short JWT proof signed by the matching private key, covering the method, URI, and a hash of the token — so a stolen token is useless without the key."
---

## TL;DR

A **bearer token** — a session cookie, a JWT, an API key, a workload-identity token — grants access
to *whoever bears it*. If it leaks, it's replayable from anywhere until it expires. **Sender
constraint** fixes that: bind the token to a **holder key** so that *using* it requires proving
possession of a private key the token names. The standard mechanism is **DPoP** ([RFC 9449](ref:rfc9449)):
the token carries a confirmation claim (`cnf.jkt`) naming the holder's public-key thumbprint, and
each request includes a short, signed **DPoP proof** over the method, URI, and a hash of the token.
A token without its matching private key is useless. Default to sender-constraining any token whose
theft would matter — especially long-lived workload credentials and anything validated at a trust
boundary like a gateway.

## Context

Almost everything authenticates with a bearer token, and the defining property is right there in
the name: the token grants access to whoever holds it, with no binding to *who you are* or *what
you're asking for*. So if a token leaks — through an XSS bug, a logged `Authorization` header, a
compromised proxy, a backup left in object storage — an attacker can replay it from anywhere and do
anything the legitimate holder could, until it expires or someone notices.

Short token lifetimes reduce the window but don't close it. The structural fix is to stop the token
from being useful on its own.

Two complementary techniques address this, and they solve slightly different problems. This note is
about the first; the second has its own note:

- **Sender-constrain the token (DPoP).** Bind the credential to a key the client holds, so presenting
  the token also requires a fresh proof of possession. That's this note.
- **Sign the message itself (HTTP Message Signatures).** Sign the actual request — and ideally the
  response — so the recipient can prove who sent *this* message and that not a byte changed in flight.
  See [ZFN-7](/zfn/7-sign-the-message/). DPoP and message signing share a goal — kill pure-bearer
  replay — so pick by what you're protecting (the token, or the message).

## Recommendation

**Sender-constrain any token whose theft would matter.** Concretely, using DPoP:

- **Name the holder key in the token.** When the token is issued, the client presents a public key;
  the issuer embeds its thumbprint in a confirmation claim (`cnf.jkt`). The token is now bound to
  that key.
- **Require a proof of possession on every request.** The client sends a `DPoP` header: a short
  JWT, signed by the holder's private key, that covers the HTTP method, the target URI, a timestamp/
  nonce (anti-replay), and a hash of the access token (`ath`). The server verifies the proof's
  signature against the key named in the token, and that the method/URI/token-hash match the actual
  request.
- **Validate it at the boundaries that matter.** Verifying possession at the front door (an edge
  gateway) and at sensitive services keeps the credential sender-constrained end to end, not just at
  issuance.
- **Hold the holder key carefully.** The whole guarantee rests on the private key staying private.
  A long-lived client that keeps the key resident (e.g. minting proofs on a hot path) must bound key
  residency and handle rotation/eviction; never log it.

The point is narrow and worth stating plainly: **a stolen token, without the matching private key,
buys the attacker nothing.**

## Consequences

**Easier:**

- Token theft stops being game-over. A leaked token (logs, a proxy, a backup) isn't replayable
  without the holder key.
- Pairs cleanly with short-lived, platform-issued identity tokens
  ([ZFN-5](/zfn/5-platform-workload-identity-service/)) to give theft resistance uniformly.

**Harder:**

- Clients must hold a private key and sign every request — more moving parts than attaching a bearer
  string, and a key-management responsibility on the client side.
- Proof verification adds work on the hot path (a signature check per request), and clock-skew/nonce
  handling needs care to avoid both replay windows and false rejections.
- It constrains *theft of the token*, not *compromise of the client that holds the key*. If the
  attacker has the private key, they have the credential — sender constraint raises the bar, it
  doesn't remove the need to protect the key.

**New obligations:**

- Decide, per credential, whether its theft matters enough to sender-constrain — and default to
  "yes" for long-lived and high-value tokens.
- Where you hold holder keys, document and bound their custody (residency, rotation, eviction,
  no-logging).

## References

- [RFC 9449 — OAuth 2.0 Demonstrating Proof of Possession (DPoP)](https://www.rfc-editor.org/rfc/rfc9449).
- [ZFN-7](/zfn/7-sign-the-message/) — signing the message itself (HTTP Message Signatures), the alternative for when you want to bind the *message* rather than constrain the *token*.
- [ZFN-5](/zfn/5-platform-workload-identity-service/) — the short-lived identity tokens this note binds to a key.

## Changelog

- **2026-06-12**: First published as a Field Note.
