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.
- Token resale markets exist. Bot operators buy and sell short-lived API tokens on private Telegram channels and Discord servers. The harvester does one expensive thing (solve a captcha, render the JS, click through a consent flow); the buyers do the cheap thing (replay).
- CDN bot scoring is per-token, not per-request. If your edge issues a token after a Cloudflare Turnstile check and downstream API trusts the token, an attacker who clears Turnstile once can drain the budget that the token authorises.
- Pre-auth surfaces are everywhere. Marketing-page LLM costs, public search APIs, content scraping endpoints, signup flows — all live before the user is known and after the page is loaded. HMAC raises the floor; it doesn't bind the credential.
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
Step 1 — generate a keypair
Step 2 — start an anonymous session
Step 3 — call the protected endpoint
Attack scenarios
Verdict
What this is not
| Anonymous DPoP is | Anonymous 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. |
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.
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.