DPoP — Sender-Constrained Access Tokens (RFC 9449)
DPoP — Sender-Constrained Tokens
Section titled “DPoP — Sender-Constrained Tokens”DPoP makes a stolen access token useless without the matching private key.
Rakomi implements RFC 9449 DPoP
(Demonstrating Proof of Possession at the Application Layer). When you enable
DPoP for an OAuth client, every access token issued to that client carries a
cnf.jkt confirmation claim (RFC 7800) that binds the token to a client-held
ephemeral key pair. Every request to a Rakomi resource-server endpoint must
present a fresh DPoP-proof JWT signed by that key — a stolen bearer-string
without the matching private key cannot forge a valid request.
Threat model
Section titled “Threat model”DPoP defends against:
- Token exfiltration via XSS, leaked log lines, browser-extension exfil
- Network MITM that captures a token (TLS downgrade, hostile proxy)
- Server-side store compromise where issued access tokens leak
DPoP does NOT defend against:
- Full client compromise (the attacker has the private key)
- Social-engineering of a fresh login flow
- Server-side signing-key compromise
- Refresh-token theft — refresh tokens are NOT DPoP-bound in this release. Binding them is on the roadmap. For the layers that DO cover refresh-token theft today, see the revocation epoch and refresh-token reuse detection.
Enabling DPoP on a client
Section titled “Enabling DPoP on a client”DPoP is opt-in per OAuth client via the dpop_mode setting, with three states:
| Mode | Behavior |
|---|---|
off (default) | Legacy Bearer behavior — no behavioral change, no DPoP processing. |
observe | DPoP proof validated when present; absence is allowed and audited via auth.dpop_proof_absent_observe. Use this to validate your fleet’s DPoP readiness before flipping to required mode. |
on | DPoP proof REQUIRED on every token-endpoint POST and on every resource-server call. |
Tenant admins flip the mode from the dashboard
(/[tenant]/settings/dpop). For client_type='public' clients, only off
and observe are available — RFC 9449 §5 mandates refresh-token binding for
public clients with DPoP, and that work is explicitly deferred.
Using the @rakomi/node SDK
Section titled “Using the @rakomi/node SDK”The SDK ships a createDpopProver helper that generates an ephemeral ECDSA
P-256 keypair lazily on first use, then signs a fresh proof JWT on every
request.
import { RakomiClient, createDpopProver } from '@rakomi/node';
const prover = createDpopProver({ baseUrl: 'https://api.rakomi.com' });
// On every token-endpoint POST:const proof = await prover.proof('POST', '/oauth/token');const tokenRes = await fetch('https://api.rakomi.com/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', DPoP: proof }, body: new URLSearchParams({ grant_type: 'authorization_code', /* ... */ }),});const { access_token, token_type } = await tokenRes.json();// token_type === 'DPoP' when the client has dpop_mode='on'
// On every resource-server call (note the `ath` binding):const apiProof = await prover.proof('GET', '/v1/users/me', { accessToken: access_token });await fetch('https://api.rakomi.com/v1/users/me', { headers: { Authorization: `DPoP ${access_token}`, DPoP: apiProof },});Caveats
Section titled “Caveats”- The ephemeral keypair is process-scoped. Sharing the same prover across worker threads / processes voids the sender-constraint guarantee.
- On serverless runtimes (Lambda, Vercel, CF Workers) the keypair lifetime matches the handler-instance lifetime. Expect token churn at cold-start frequency.
- Algorithm allow-list. Rakomi accepts
ES256(ECDSA P-256) andEdDSA(Ed25519). RS256 is explicitly excluded — ephemeral RSA-2048 keys are bandwidth-heavy in the proof’sjwkheader and RFC 9449 §11.4 implicitly assumes EC/OKP. - Refresh-token-public-vs-confidential split. Some peers (e.g. Keycloak) bind refresh tokens for public clients but not confidential. Rakomi defers refresh-token binding entirely in this release; confidential clients see Bearer refresh tokens identically to pre-DPoP behavior.
Authorization-server metadata
Section titled “Authorization-server metadata”The discovery documents advertise DPoP support:
{ "dpop_signing_alg_values_supported": ["ES256", "EdDSA"]}This field is emitted on /.well-known/oauth-authorization-server and
/.well-known/openid-configuration.
Authorization-code binding (RFC 9449 §10)
Section titled “Authorization-code binding (RFC 9449 §10)”A DPoP-aware client can bind its key to the authorization code at authorize-
time by passing dpop_jkt=<thumbprint> as a query parameter. At the
/oauth/token code-exchange the proof’s JKT MUST equal the stored value or
the request is rejected as invalid_dpop_proof. This defends against the
classic code-injection threat (OAuth Security BCP §4.10).
Server-issued nonces (RFC 9449 §8)
Section titled “Server-issued nonces (RFC 9449 §8)”Not enabled yet. When server-issued nonces are enabled for a client, Rakomi
responds to nonceless proofs with
WWW-Authenticate: DPoP error="use_dpop_nonce", nonce="<value>" and the SDK
transparently retries with the issued nonce — so a client built on the SDK needs
no change to be ready for it. Handle that challenge in a hand-rolled client and
you are ready too.