Deep DivesSender-Constrained Tokens

Sender-Constrained Tokens

The single most important concept for this project, because it’s the direct heir to the mobile bank’s per-request hash. FAPI 2.0 mandates it; see the rulebook.

Bearer vs proof-of-possession

A bearer token is like cash or a concert wristband — whoever holds it spends it. Steal it (from a log, an XSS payload, a leaky proxy) and you have full access. This is the biggest web-specific risk, because the browser leaks tokens in ways a native app doesn’t.

A sender-constrained token (proof-of-possession, PoP) is like a named ticket you must show ID for. The token is bound to a key the legitimate client holds, and every use requires proving possession. A stolen token alone is worthless.

This is the exact web analog of the mobile per-request hash. On mobile the enclave key signs each request; on web a client-held key proves possession on each call. Same principle, different key store.

See it happen — steal a bearer token, then steal a DPoP one:

Legitimate client
Holds the access token ✓
Nothing else needed
API call → 200 OK
Attacker who stole the token
Holds the access token
Nothing else needed
The lesson: a bearer token is like cash — whoever holds it spends it, so a stolen one grants full access. A sender-constrained token (DPoP) is bound to a private key the client proves it holds on every call. Steal the token and you still fail, because the key never left the legitimate client. This is the web analog of the mobile per-request hash.

DPoP — Demonstrating Proof of Possession (RFC 9449)

Application-layer:

  1. The client generates its own key pair — in the browser via WebCrypto, ideally non-extractable in IndexedDB.
  2. On every request it builds a small DPoP proof JWT, signed with the private key, carrying the HTTP method (htm), URL (htu), timestamp (iat), a unique id (jti) for replay protection, and a hash of the token (ath). It rides in the DPoP header.
  3. At issuance the AS binds the token to the thumbprint of the client’s public key — the cnf.jkt claim.
  4. The gateway, on each call, checks: proof signature valid, htm/htu match, the proof’s key thumbprint equals the token’s cnf.jkt, jti/iat fresh.

Steal the token and, without the private key that never leaves the client, you can’t forge a matching proof.

Analogy: you hand-sign a fresh note for each request proving you still hold the key.

mTLS-bound tokens (RFC 8705)

Transport-layer. The client holds an X.509 certificate, opens a mutually-authenticated TLS connection, and the AS binds the token to the certificate thumbprint (cnf.x5t#S256). Every call presents the same cert; the gateway checks it matches. The cert doubles as strong client auth.

Analogy: the whole phone line is locked to your certificate; the token only works on that line.

The comparison that drives the decision

DimensionDPoPmTLS
LayerApplication (HTTP + JWT)Transport (TLS)
Key storeBrowser WebCryptoCert store / HSM
Provisioning to millions of browsersTrivial — key made in-browserImpractical — certs don’t distribute to consumers
Behind a TLS-terminating CDNWorks — binding is app-layerFragile — cert must be forwarded carefully
Infra neededNone beyond codeFull PKI: issuance, rotation, revocation
Best fitConsumer web / SPAB2B, server-to-server, Open Banking TPPs

Recommendation for a consumer web bank: use DPoP for the browser channel and keep mTLS for server-side and B2B/TPP paths. FAPI 2.0 accepts either, so this is architecture, not compliance.

The decision underneath: where do tokens live?

A browser SPA is a public client — it can’t safely hold a secret or a cert. Two patterns:

  1. BFF (Backend-for-Frontend). A server-side confidential client runs the OAuth flow and holds the tokens; the browser holds only an HttpOnly, SameSite cookie. Tokens never touch the browser — the whole token-theft-from-browser risk disappears. Usually the stronger posture for a bank, and it resolves the mTLS provisioning problem too.
  2. SPA + DPoP. Tokens live in the browser, DPoP-bound to a non-extractable key. Simpler topology, but you now trust the browser to hold keys safely.

The whole flow assembled

This single spine connects C1 (the passkey at step 3), C2 (the DPoP binding at 5–7) and C5 (gateway validation at 7).

Client crib-sheet

  • Bearer = cash. Whoever holds it spends it.
  • Sender-constrained = named ticket + ID. Useless if stolen.
  • DPoP = sign a fresh note per request proving you hold the key. (the web version of the mobile hash)
  • mTLS = the whole line is locked to your certificate.