Here’s the 2 a.m. version of the problem. A laptop gets popped, an infostealer rakes the token cache, and six weeks later a refresh token you forgot existed is quietly minting access tokens from an IP in a country your users have never visited. Bearer tokens are called bearer tokens for a reason: whoever bears them wins. For the better part of a decade that has been the whole game.
Google just changed the rules for its own token endpoint. Here’s how we got here.
September 2023 — RFC 9449 lands
The IETF publishes Demonstrating Proof of Possession. The idea is almost insultingly simple: the client generates a key pair, keeps the private key, and ships the public key to the authorization server when it asks for a token. The server stamps a thumbprint of that key (cnf.jkt) into the token. From then on, every request has to carry a fresh signed proof that you still hold the matching private key. No key, no proof, no token. The token stops being a password you can copy and becomes a lock you have to physically own.
The mechanic — a signed JWT per request
The proof is a short-lived JWT in a DPoP header. Header carries typ: dpop+jwt and the public JWK. Body binds the proof to this exact call: htm (method), htu (URL), iat, a random jti, and ath (a hash of the access token) once you’re hitting a resource. Replay it against a different endpoint and the signature check fails. That’s the part people miss — it’s not one key operation, it’s one per request.
October 2026 — Google turns it on
Google now accepts DPoP at oauth2.googleapis.com/token and binds the issued refresh token to your key. You send the proof on the authorization-code exchange and on every refresh. The response comes back "token_type": "DPoP" instead of Bearer, which is your signal the binding took. The refresh exchange looks like this:
Decoded, that proof is nothing exotic:
ES256 keys, P-256 curve. Generate once, store the private key somewhere malware can’t trivially scrape — a TPM, a secure enclave, a KMS. If you leave it in the same config.json as the refresh token, you’ve built a very elaborate way to achieve nothing. The whole security argument collapses the moment both halves live in the same stealer-readable file.
What it does not fix
DPoP kills exfiltration and replay. It does nothing about phishing. If a user is tricked into authorizing your attacker’s app, that app holds the private key legitimately and DPoP waves it right through. It also doesn’t stop token theft on a box the attacker already fully controls — if they can sign proofs on the live machine, binding is cosmetic. It raises the cost from “copy a string” to “maintain persistent code execution on the endpoint,” which is a real and meaningful jump, not a magic ward.
Migration — opt-in, and it stays that way
Nothing breaks. Existing integrations keep getting bearer tokens because they don’t send a DPoP header, and Google keeps issuing them. You adopt it per client, at your pace: generate a key, attach the proof, start handling 401 responses carrying WWW-Authenticate: DPoP with a nonce you must echo back in your next proof’s nonce claim. That nonce dance is the bit that trips first implementations — Google can demand a server-chosen nonce, and if you don’t replay it, every call 401s in a loop. Build the retry before you ship, not after the pager goes off.
The next dated beat isn’t on Google’s calendar. It’s on yours — the day you audit which of your service integrations still hand out tokens that work from anywhere on earth, and decide how many more 2 a.m. incidents you’re willing to underwrite before you rotate them onto a key.
