Demo · Anonymous DPoP (RFC 9449 + RFC 9577)

Authenticate the browser, not the user

A third pattern that sits between the HMAC anti-abuse gate (Part 1, no key state) and authenticated DPoP (Part 3, post-login). This one binds a session token to a non-extractable key the browser holds, before the user has authenticated. The session is anonymous from byte zero — but it's still tied to the originating device, so a token that gets harvested off one page load can't be moved to other machines.

The gap HMAC tokens leave

The Part 1 HMAC demo proves "this browser loaded the page recently" — enough to make scraping more expensive, not enough to make a harvested token useless. The token itself is a bearer instrument: an HMAC over nonce | expiry, no key binding, no per-request signature. A bot that does one page load and sells the harvested token to 1,000 other bots gets a 1,000x multiplier on the work — every buyer can mint lookups until the token expires.

What anonymous DPoP does

Same primitive as full DPoP (RFC 9449), applied before authentication. On page load the browser generates a non-extractable WebCrypto keypair and posts the public JWK to a "start session" endpoint. The server computes the RFC 7638 thumbprint, mints a short-lived session JWT with cnf.jkt set to that thumbprint, and returns it. Every subsequent API call carries the session JWT in Authorization: DPoP <token> plus a freshly-signed DPoP proof in the DPoP header. The same RFC 9449 §4.3 verifier runs every check including cnf.jkt and ath — no separate code path.

POST /api/v1/anon-session/start HTTP/1.1
Content-Type: application/json

{"jwk": {"kty":"EC","crv":"P-256","x":"…","y":"…"}}      ← the public half of a non-extractable keypair

HTTP/1.1 200 OK
{
  "access_token": "eyJhbGciOiJIUzI1NiJ9…",                ← HS256 JWT with cnf.jkt = thumbprint(JWK)
  "token_type":   "DPoP",
  "expires_in":   600
}

The crucial difference from Part 1's HMAC token: the access token cannot be replayed by another machine, because the other machine doesn't have the private key. Apple's Private Access Tokens (RFC 9577) and several CDN bot-management products use the same pattern in production — bind credibility to the device, not to the human.

Try it · the full anonymous flow

Three steps. Generate a keypair (extractable=false), start an anonymous session, call a protected endpoint. The protected endpoint is the same /api/v1/dpop-verify the authenticated demo uses — that's the whole point: the verifier doesn't care whether the session came from a login or from an anonymous-session start, it only cares that the proof binds.

Step 1 — generate a keypair

The private key never leaves the browser — generated with extractable: false, so even a same-origin script (think: injected XSS) cannot read it back out via exportKey. Only WebCrypto itself, via subtle.sign, ever touches the private half. The public key is exported as a JWK and POSTed to the start endpoint so the server can compute the RFC 7638 thumbprint and bind cnf.jkt.

no key yet

Step 2 — start an anonymous session

POSTs the page's public JWK to /api/v1/anon-session/start. The server computes the RFC 7638 thumbprint, mints a 10-minute HS256 session JWT with cnf.jkt set to that thumbprint, and returns it. The page keeps the token in module-scope memory — closing the tab forgets it, matching what a real anonymous-DPoP client would do.

no session
(no session yet — generate a keypair, then click Start anonymous session)

Step 3 — call the protected endpoint

POSTs to /api/v1/dpop-verify with Authorization: DPoP <session_token> and a fresh DPoP proof bound to the token via ath. The server runs every RFC 9449 §4.3 check, including the cnf.jkt binding and the per-request ath claim.

Attack scenarios

Three failure modes anonymous DPoP defends against. Each button runs the attack against the just-issued anonymous session and shows you the per-check verdict — the failure is always loud.

Verdict

(no result yet)

What this is not

Anonymous DPoP isAnonymous DPoP is not
A way to bind a pre-auth session credential to the originating device's non-extractable key, so a harvested token cannot be moved to other machines. A replacement for gating the start endpoint. The /anon-session/start endpoint itself is unauthenticated and must still be rate-limited — at minimum per-IP, and in production probably combined with HMAC or Turnstile. This demo's start endpoint uses the same per-IP token bucket the rest of the API uses.
Compatible with modern browsers (Chrome 37+, Firefox 28+, Safari 11+) via the WebCrypto API. Non-extractable keys are supported everywhere WebCrypto's generateKey is. A guarantee against a fully-compromised browser. If the attacker can read the private key (extractable JWK, browser extension with full DOM access, OS keychain leak), they can mint proofs at will. Non-extractable WebCrypto keys raise the cost; they don't eliminate it.
Production today via Apple's Private Access Tokens (RFC 9577), several CDN bot-management products, and any custom integration of RFC 9449 to a pre-auth surface. A drop-in replacement for HMAC anti-abuse. Anonymous DPoP costs the browser a WebCrypto signature per request (~1-2ms on a modern laptop, invisible but real). For low-value, high-volume endpoints — a public weather API, a homepage hit counter — the HMAC pattern is still the right tool. Reach for anonymous DPoP when the per-token blast radius matters.
Defence against jti replay (the same proof submitted twice gets rejected on the second submission). Defence against the user being a bot. The whole point of binding to a device is to make harvested tokens worthless to other machines — but the originating browser itself can still be a bot. Pair with bot-scoring on the start endpoint if you need user-not-bot signal too.
Honest about its scope: it's a layer that makes credential theft and credential resale much harder, not a single mechanism that solves the whole pre-auth problem. RS256/ES256 with a published JWKS — this demo uses HS256 to keep the issuer + verifier in the same binary for pedagogical clarity. Production would split issuer and resource server, sign with an asymmetric key, and publish a JWKS endpoint so the resource server doesn't share the issuer's signing secret.
UX tradeoff: the same as Part 3 (authenticated DPoP) — every API call now needs a fresh proof, which means every call hits SubtleCrypto.sign. ES256 on a modern laptop is ~1-2ms, invisible to the user, but the proof generation is async and your client SDK has to thread it into every request. Hand-rolled fetch wrappers will need a small helper; auth libraries like oauth4webapi or jose's DPoP helpers hide it for you.
Anonymous DPoP changes the economics of token harvesting. The Part 1 HMAC pattern says "you have to load the page" — one harvester serves a thousand replayers. Anonymous DPoP says "you have to load the page and hold the key" — and because the key is non-extractable, harvesting the key out of WebCrypto is materially harder than scraping the token out of a logging proxy. The asymmetry is the whole point: defenders make 1,000 replays into 1,000 fresh page-loads-plus-keypair-generations, each with whatever bot-scoring the start endpoint enforces.

Need this designed into your pre-auth surface?

We help product and security teams pick the right credential pattern for each surface — HMAC anti-abuse for low-value high-volume, anonymous DPoP where token resale would hurt, full DPoP for authenticated APIs — and ship them without breaking the clients you already have.