awesome-everything RU
↑ Back to the climb

Security

Sender-constrained tokens: DPoP and mTLS

Crux Why bearer tokens fail under XSS or supply-chain attack, how DPoP binds a token to a client-held key, and when to use mTLS-bound tokens instead — with FAPI 2.0 requirements.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 11 min

A supply-chain attack injects one line of JavaScript into your SPA. It reads the access token from memory and exfiltrates it to an attacker’s server. The token is a bearer token — whoever holds it wins. The attacker now has minutes or hours to call your API as the victim. DPoP makes the stolen token worthless: without the corresponding private key that never left the client, the attacker cannot generate a valid proof.

The bearer token problem

A bearer access token is exactly what the name says: anyone who holds it can use it. The resource server does not check who is presenting the token — only that the token is valid. This is the design.

The consequence: theft equals full compromise for the token’s remaining TTL. Short TTLs (5–15 min) limit the window, but they do not eliminate it. For banking, healthcare, and other high-value workloads, “a stolen token works for 15 minutes” is not an acceptable risk.

DPoP — Demonstrating Proof of Possession (RFC 9449)

DPoP binds the access token to a key pair controlled by the client. The client generates a non-extractable key pair (WebCrypto in browsers, OS keychain in native apps). The access token carries a cnf (confirmation) claim containing the JWK thumbprint (jkt) of the public key.

On every API call, the client computes a DPoP proof JWT in the DPoP HTTP header. The proof payload contains:

  • jti — unique ID (server caches to detect replays, typical window 5–10 min)
  • htm — HTTP method (e.g. POST)
  • htu — HTTP target URI (e.g. https://api.bank.example/v1/transfer)
  • iat — issued-at timestamp (server rejects if outside ~60s window)
  • ath — SHA-256 of the access token (binds this proof to this specific token)

The proof’s header includes the public key (jwk). The resource server validates:

  1. Signature valid (using the embedded jwk)
  2. ath = SHA-256 of the incoming bearer token
  3. htm and htu match the actual request
  4. iat within acceptable window
  5. jti not seen recently (anti-replay)
  6. jwk hashes to the jkt in the token’s cnf claim

A stolen token without the private key cannot generate step 1 or 5. The API call is rejected.

DPoP proof structure per API call
DPoP header (JWT signed with client private key)
header: { alg: ES256, jwk: {public key} }
payload: { jti: uuid, htm: “POST”, htu: “https://api.bank…/transfer”, iat: 1716230400, ath: sha256(access_token) }
Access token cnf claim
cnf: { jkt: “base64url(SHA-256(public_key))” }
Resource server: verify proof signature, check ath, check htm+htu, check jki = jkt in token. Stolen token without private key fails step 1.

mTLS-bound tokens (RFC 8705)

mTLS (mutual TLS) binds the access token to the client’s X.509 certificate. The TLS handshake itself presents the certificate; the resource server verifies that the TLS-presented certificate matches the cnf.x5t#S256 claim in the token.

DPoP vs mTLS — the practical difference for browser SPAs:

  • Browsers do not expose TLS client certificates to JavaScript. mTLS requires the certificate to be selected at the OS / browser level — not practical for SPAs where the key must live in JS.
  • DPoP uses the WebCrypto API to hold a non-extractable key pair in the browser JS context. This works; the key never leaves the browser but is still controlled by the SPA code.
  • FAPI 2.0 (Open Banking profile) requires sender-constrained tokens via either mTLS or DPoP. For new browser-based fintech in 2026, DPoP is the default choice. mTLS is the choice for backend service-to-service where client certificates are manageable.

PAR and JAR: hardening the authorize request

Pushed Authorization Requests (PAR, RFC 9126): Instead of putting all parameters in the browser redirect URL, the client POSTs them to a /par endpoint over a TLS-authenticated back channel first. The server returns a short-lived request_uri. The client redirects the browser to /authorize?request_uri=... — no parameters visible in the URL.

Benefits: parameters are not in browser history, referrer headers, or screenshots. The authorization server validates the request before the browser is involved.

JAR (JWT Authorization Requests, RFC 9101): The client packages the authorize parameters as a signed JWT. The auth server validates the signature before processing. Combined with PAR, the entire request is off-URL and integrity-protected.

Both PAR + JAR are required by FAPI 2.0. Consumer OAuth typically skips them; banking and healthcare implementations require them.

Quiz

Why is DPoP preferred over mTLS for browser SPAs even though both are FAPI 2.0 compliant?

Quiz

A DPoP proof is valid but the jti was seen by the server 2 minutes ago. What should the server do?

Order the steps

Order the DPoP validation steps the resource server performs:

  1. 1 Parse the DPoP JWT header and extract the embedded public key (jwk)
  2. 2 Verify the DPoP proof signature using the embedded public key
  3. 3 Verify ath claim equals SHA-256 of the incoming access token
  4. 4 Verify htm and htu match the actual HTTP method and URI of this request
  5. 5 Verify iat is within the acceptable time window (~60 seconds)
  6. 6 Check jti has not been seen in the replay cache (5–10 minute window)
  7. 7 Extract cnf.jkt from the access token and verify the proof's jwk hashes to that value
Recall before you leave
  1. 01
    What makes a DPoP proof bind a stolen access token to uselessness?
  2. 02
    What does PAR (Pushed Authorization Requests) add to the OAuth flow?
  3. 03
    When would you choose mTLS over DPoP for sender-constraining tokens?
Recap

Bearer tokens are the legacy default: anyone who holds the token can use it. DPoP (RFC 9449) binds the token to a client-held private key — every API call requires a fresh proof JWT signed by that key, containing the hash of the access token, the request method, and the target URI. Stolen tokens without the key are rejected. mTLS-bound tokens (RFC 8705) achieve the same binding via X.509 client certificates. FAPI 2.0 requires one or the other for banking, healthcare, and regulated AI workloads. DPoP is the browser choice because WebCrypto provides non-extractable keys in JavaScript; mTLS is the backend-service choice. PAR and JAR harden the /authorize request itself by moving parameters off the URL.

Connected lessons
appears again in202
Continue the climb ↑OAuth in production: audience attacks, observability, and real failures
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.