DPoP, defined in RFC 9449, binds OAuth access tokens to a cryptographic key held by the client, so a stolen token is useless without the private key that signs each request using it. It converts bearer tokens, which anyone holding them can replay, into possession-bound tokens, closing the token-theft class of attacks.
The trade is proof-of-possession for client complexity. Every request carries a fresh signed proof bound to the method, the URL and a nonce, so the client needs a key it can keep and use on each call, and the server needs replay protection for the proofs. In exchange, a token lifted from a log, a proxy or browser storage is inert.
The class of attack it closes is worth naming concretely, because bearer-token theft rarely looks like an exploit. Tokens end up in server logs, in proxy traces, in browser storage reachable by injected script, and in error reports; anyone who reads one can use it until it expires. Binding the token to a key the client holds makes all of those copies inert.
The cost lands on both sides. The client needs a key it can generate, keep and sign with on every call, which is straightforward in a native application and more constrained in a browser; the server needs to verify a fresh proof per request and reject replays, which means tracking nonces or jti values with the storage and expiry that implies.
DPoP is a possession proof about the client software rather than about the hardware, so it sits alongside rather than inside device binding: how strongly the key is held depends on where it lives, and a key in ordinary process memory is defeated by an attacker who already has code execution there.
Scoping matters too: a proof bound to the method and URL limits a captured proof to the exact call it was made for.
Binding a token to a key the client holds complements, rather than replaces, the origin binding a passkey provides at authentication time.
Go deeper
Related terms
PasskeyBridge verifies identity signals like these inside the request, with zero PII stored. See how the platform works or test the live API.