PasskeyBridge —

Security · 2026-04-28

DPoP Token Binding: Beyond Bearer Tokens in 2026

By J. W. Bouckaert

DPoP Token Binding: Beyond Bearer Tokens in 2026

Bearer tokens

For two decades, OAuth 2.0 has been built around one assumption: whoever presents the token is the client. That is the literal definition of a bearer token in RFC 6750: "any party in possession of a bearer token (a 'bearer') can use the token in any way that any other party in possession of it can." For most of OAuth's lifetime, deployments relied on TLS to keep tokens off the wire and on the same-origin policy to keep them out of other sites.

Neither held up. Browser extensions exfiltrate Authorization headers. Service-mesh debug proxies log them. SSO replay attacks pivot from a single stolen access token into lateral movement across every API the user touches. RFC 9449 (DPoP), published as a Proposed Standard in September 2023, exists precisely because the bearer-token model cannot distinguish a legitimate client from an adversary that has obtained a copy of the token. DPoP is the IETF's standards-track answer to that problem.

This article walks through what DPoP actually does, why it matters more in 2026 than it did in 2023, and how PasskeyBridge runs the rollout in production. If you want to skip the theory and just turn it on, jump to the Enable DPoP Shadow Mode in 5 Minutes quickstart; the DPoP Proof-of-Possession reference guide covers the full lifecycle once you are ready to enforce.

DPoP

DPoP (Demonstrating Proof of Possession) is a sender-constraint mechanism for OAuth 2.0. It does one thing: it binds an access token to a key the client holds, so presenting the token alone is no longer sufficient to use it.

The mechanics, stripped to essentials:

  1. The client generates an asymmetric key pair locally (for example ES256). The private key never leaves the client.
  2. On every authenticated request, the client signs a short-lived JWT, the "DPoP proof", that asserts the HTTP method, the target URI, an iat timestamp, and a hash of the access token. The proof is attached as a DPoP header.
  3. The resource server verifies the proof's signature against the public key embedded in the proof's jwk header, confirms the proof's claims match the actual request, and confirms the proof's key matches the key the access token was originally bound to (via the JWK Thumbprint in the token's cnf.jkt claim).

If any of those checks fail, the request is rejected, even if the access token itself is valid. Presenting the token is no longer enough; the client must also prove on every request that it holds the bound key.

PropertyBearer Token (RFC 6750)DPoP-Bound Token (RFC 9449)
Stolen header alone is usableYesNo
Replayable across originsYesNo
Client must hold a private keyNoYes
Per-request cryptographic proofNoYes
Server-side state requiredToken introspection onlyToken + JWK Thumbprint binding
Standardized2012September 2023

The trade-off is operational. DPoP requires the client to manage a key pair, sign a proof on every request, and handle nonce rotation when the server demands it. For browser SPAs, that means a non-extractable Web Crypto key in IndexedDB. For mobile apps, it means a Secure Enclave or Keystore-resident key. For backend confidential clients, it means treating the DPoP key with the same care as the client secret.

Whether that work is justified depends on the threat model, which has changed since 2023.

Adoption timing

DPoP was published as a Proposed Standard in September 2023. Through 2024 and 2025 the main browser attack, same-device session hijack via XSS, was largely handled by HttpOnly cookies and SameSite=Lax defaults, which weakened the case for DPoP.

Three things have changed.

First, agentic identity is now in production. When an autonomous AI agent acts on a user's behalf, the agent is presenting access tokens to APIs the user has never touched in this session. Browser protections such as the same-origin policy do not apply to an agent running as a server process. The scope-poisoning attack class we documented earlier this year, where a compromised agent uses a legitimately issued token in ways the user never intended, is a bearer-token problem. Binding the token to a key the agent holds makes a copied token unusable without that key.

Second, browser extensions can still read authorization headers. The Chrome Manifest V3 transition was supposed to narrow the surface for token-stealing extensions, but an extension that users grant the right host permissions can still observe request headers. DPoP makes a stolen header useless without the corresponding key.

Third, high-assurance profiles require it. The FAPI 2.0 Security Profile, used for open banking and other high-value APIs, requires sender-constrained access tokens, using either mutual TLS or DPoP.

The PasskeyBridge implementation

The full implementation specification lives in our DPoP Proof-of-Possession guide. Every PasskeyBridge access token can be optionally bound to a client-held DPoP key, with five edge-function actions covering the lifecycle: bind, verify, nonce, introspect, and cleanup. Bindings are tenant-scoped, JWK Thumbprints (per RFC 7638) serve as the stable binding identifier, and access tokens are stored only as SHA-256 digests to maintain our zero-PII architecture commitment. Access tokens are random and high-entropy, so a plain SHA-256 digest of one cannot be reversed by enumeration; our identifier hashing post explains why low-entropy identifiers such as phone numbers need keyed HMAC-SHA-256 instead.

The proof validation pipeline enforces:

  • typ: "dpop+jwt" header, which rejects generic JWTs masquerading as proofs
  • alg from a strict allowlist (ES256, ES384, ES512, PS256, PS384, PS512); none and HS\* are refused
  • htm (HTTP method) and htu (HTTP URI) match the actual request, which prevents cross-endpoint replay
  • iat within a 60-second skew window, which bounds replay opportunity
  • jti uniqueness via a tenant-scoped seen-set, which prevents proof replay within the skew window
  • ath (access token hash) matches SHA-256(access_token) when an access token is presented, which binds the proof to that specific token
  • Server-issued nonce when the resource indicates one is required, which defeats pre-computed proof attacks

When verification fails, shield-dpop returns a JSON error body with a code and a reason. It does not set the WWW-Authenticate header described in RFC 9449 section 7.1. When a nonce is required and the proof lacks a valid one, the error code is DPOP_NONCE_REQUIRED and the body carries a fresh nonce in dpop_nonce; the client retries with a new proof that includes it.

Re-attestation

A DPoP-bound token stays bound until the access token expires, so a key extracted from a compromised browser extension or device can be used for the rest of that token's lifetime. shield-dpop applies no re-attestation interval to bindings. Re-attestation is a separate function, shield-reattest, which tracks sessions, agents and API keys on a 15-minute cadence. The caller reports each re-attestation as passed or failed; after two consecutive failures the subject moves to a shadow_degraded tier, which is logged and does not block requests.

Shadow mode

The single biggest reason teams put DPoP off is the rollout risk. The moment you flip enforcement on, every client that has not yet shipped DPoP support starts getting 401s. For a SaaS API with thousands of integrators on different release cadences, that is a coordinated-release problem the platform team cannot solve alone.

DPoP shadow mode is on for every organization. Every request that passes through the shared API-key authentication module is inspected for a DPoP header. The header's presence, structural validity and failure reason (if any) are logged to the shield_dpop_shadow_log table without affecting the request's outcome. The write runs in a queued microtask and a failed write is swallowed, so it never changes the response. shield-ingest authenticates separately and is not observed.

What we collect, per-tenant:

FieldPurpose
dpop_presentAdoption baseline: the fraction of clients are sending proofs at all
dpop_validValidity baseline: the share of proofs that are well-formed
failure_reasonDiagnostic: proof_too_large, malformed_jwt, wrong_typ, missing_alg, missing_jwk, missing_htm_htu, missing_iat, parse_error
endpoint + http_methodPer-route adoption; some endpoints are easier to migrate first
client_ip_hashKeyed HMAC-SHA-256 source IP (server-held pepper), for rate-limit and concentration analysis without storing PII, and non-enumerable unlike a plain SHA-256 of a 32-bit IPv4 address
tenant_idPer-tenant rollout visibility

Tenant admins can read their own rows in shield_dpop_shadow_log, and the shield_get_dpop_adoption database function returns adoption and validity percentages per endpoint for a time window. No dashboard tab renders them yet. Enforcement is a separate path: the bind and verify actions on shield-dpop.

If the shadow recorder errors, the request still succeeds. The cost is that some telemetry events are lost when a database write fails; the alternative would let telemetry failures cause authentication failures.

DPoP in the identity stack

DPoP is one layer in a stack:

  • Authentication: the user proves who they are, typically via WebAuthn passkey biometric binding.
  • Token issuance: the authorization server issues an access token, optionally bound to a DPoP key.
  • Token presentation: the client presents the token + a fresh DPoP proof on every API request (this is what RFC 9449 covers).
  • Cross-IdP propagation: when a session is revoked, SSE/CAEP signals propagate the revocation to the relying parties that receive them. Running CAEP in production covers signing the outbound Security Event Tokens and how receivers handle them.
  • Re-attestation: a session, agent or API key is re-attested on a schedule and failures are recorded. In PasskeyBridge this is shield-reattest, separate from DPoP.

DPoP makes a copied token unusable without the client's key. Re-attestation adds a periodic check on the session itself; in PasskeyBridge a failed re-attestation is recorded and, after two in a row, marks the subject shadow_degraded without blocking it. For AI agents, just-in-time capability tokens add single-use, narrowly scoped tokens that the resource server checks together with a DPoP proof at redemption.

Migration: A concrete path

For teams starting fresh, we recommend:

  1. Week 1-2: Enable DPoP shadow mode across all endpoints. Collect baseline adoption telemetry. Expect 0% adoption at first.
  2. Week 3-4: Ship DPoP support in your highest-traffic SDK. oauth4webapi supports DPoP; check the client libraries your integrators use.
  3. Week 5-8: Roll out DPoP support to integration partners. The shadow telemetry tells you who has adopted, who is sending malformed proofs, and who has not started.
  4. Week 9+: Per-endpoint enforcement cutover. Start with low-traffic, high-sensitivity endpoints (admin APIs, key management). Expand outward.

For teams already running PasskeyBridge, shadow mode is already on for every organization. Adoption is read from shield_get_dpop_adoption, and enforcement means calling bind and verify.

The standards trajectory

GNAP (RFC 9635), the Grant Negotiation and Authorization Protocol, makes sender-constrained tokens the default rather than an opt-in. The OAuth 2.1 consolidation draft also covers sender-constrained tokens in the base specification.

---

Implementing DPoP in production? Start with the 5-minute shadow-mode quickstart and the DPoP Proof-of-Possession reference guide, then read the SSF/CAEP receiver quickstart to see how sessions are revoked across identity providers.

Start free · Test the API