Nonces and replay handling
Suppose the photo API writes complete request headers to a debugging log, and someone with access to that log copies the app's latest request: the access token and the DPoP proof sent beside it. The token alone is useless to them, as Validating proofs at the server showed. The pair is a different matter. It is a correctly signed request, and for a short time it could be sent again.
Token theft and replay followed a copied token. Here the copy is a whole request, and replaying it needs no key at all. DPoP limits what such a replay can do, and servers that need more can close the gap almost entirely.
What a captured proof allows
The copied proof names GET, https://api.photos.example/albums/42/photos, and one specific token. The copier cannot use it for another address, another method, or another token, because the photo API compares all three. What they can do is repeat that exact request. For reading an album, that leaks the photos again. For an API that deletes photos or starts payments, a repeated request could do real harm.
Servers have two tools against it. The first is time. The specification requires servers to accept a proof only for a limited time after it was created, preferably seconds or minutes. The photo API in this example accepts proofs whose iat is no more than 60 seconds old. Clocks are never perfectly aligned, so it also tolerates an iat up to five seconds in the future. Both numbers are this example's choices, not values from the specification.
The second tool is memory. Every proof carries a unique jti. The API can record each identifier it accepts for an address and refuse any proof whose identifier it has already seen. It only needs to remember an identifier for as long as the proof would pass the time check, after which the time check rejects it anyway.
Strict tracking is very strong and not always easy. If the photo API runs on many servers with no shared storage, a replay sent to a different server will not be recognized. The API then needs a shared store, or it accepts that the time window is its only defense. A server that tracks identifiers should also reject unreasonably long ones, or store only a hash of each, so that a flood of huge values cannot exhaust its memory.
Tracking changes one habit on the client side. A retry, even of a request that would normally be safe to repeat, needs a fresh proof with a new jti and iat. Resending the old proof looks exactly like a replay and will be refused.
Proofs made in advance
The time check has a weakness: iat is chosen by whoever signs the proof. Code running inside the app, or a person who controls the device, could sign a batch of proofs with iat values set hours or days ahead, copy them elsewhere along with the token, and use them later without the key. At that point the server is checking possession of some proofs, not possession of the key.
The ath claim already limits this. A proof can only name a token that exists, so proofs made in advance stop being useful when that access token expires. With the photo service's ten-minute access tokens, a stack of pre-made proofs is worth ten minutes at most, which is one reason the specification says deployments that do not use nonces should not issue long-lived DPoP-bound access tokens. Nor can proofs be made for an access token that has not been issued yet. Someone holding a stolen refresh token and pre-made proofs might obtain a new access token, but they could not use it.
A server that wants to rule this out completely supplies a value the client cannot predict.
Server-provided nonces
A nonce here is an unpredictable value chosen by the server, which the client must copy into the nonce claim of its next proofs. No one can sign a proof containing it before the server reveals it.
At the token endpoint, a request whose proof lacks the current nonce is refused with the error use_dpop_nonce and a DPoP-Nonce header carrying the value to use:
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store
DPoP-Nonce: demo-auth-nonce-81
{
"error": "use_dpop_nonce"
}
The app retries with a fresh proof whose claims now include "nonce": "demo-auth-nonce-81". It keeps using that value in proofs for this server until the server supplies another. A server can send the next nonce in the DPoP-Nonce header of a successful response, which spares the client a rejected request each time the nonce changes.
The photo API can do the same. Because it speaks in HTTP authentication challenges, its version is a 401:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: DPoP error="use_dpop_nonce", algs="ES256"
DPoP-Nonce: demo-api-nonce-12
A few rules keep nonces meaningful:
- Each server's nonce is accepted only by that server. The app keeps one for the authorization server and another for the photo API, and never swaps them.
- Once a server has given a client a nonce, it must refuse that client's proofs without one. Otherwise an attacker would simply leave the claim out.
- A nonce may be reused across several proofs until the server replaces it. Each proof still needs its own
jti. - A server can build its nonce from its own clock, so that judging freshness no longer depends on the client's clock at all.
A DPoP nonce has nothing to do with the nonce parameter that OpenID Connect uses in authorization requests, described in State and nonce, despite the shared name.
What DPoP does not protect
Token theft and replay noted that binding protects against copies, not against an attacker who controls the client, and DPoP is no exception. Every protection in this group assumes the private key stays under the app's control. If an attacker can run code inside the app, through a compromised device, a malicious library, or injected script in a browser application, they can ask for signatures exactly as the app does. A non-exportable key stops them carrying the key away, and nonces stop them stockpiling proofs, but while their code runs they can make requests the servers will accept.
A proof also covers only the method, the address, the token, and the time. The request body and other headers rely on TLS to stay intact in transit, so an API where the body carries the important decision, such as the amount of a payment, may need message signing on top.
Within those limits, DPoP turns a token that works for anyone into one that works only alongside its key. Mutual TLS, the next group, reaches the same goal from a different layer, by binding tokens to the certificate a client presents when it opens the TLS connection.