invalid_dpop_proof and use_dpop_nonce
What each DPoP refusal means, which one is a normal part of the protocol, and how to fix a proof that is refused.
On this page
Symptom#
A call to the pushed authorization request, token, revocation, userinfo or account API endpoint, or to your own API, is refused with use_dpop_nonce or invalid_dpop_proof. If you use the SDK, the first of these is handled for you.
use_dpop_nonce is not a failure#
Identity requires a server-issued nonce inside every DPoP proof. A first request has none, so identity answers 400 with {"error": "use_dpop_nonce"} and a DPoP-Nonce header. At userinfo and the account API the same answer is a 401 with the header. You sign a new proof that includes the nonce and send the request again. Nonces rotate every 5 minutes, and the previous one is still accepted.
What to check when it does not resolve:
- You must retry once, with a new proof. Sending the same proof again fails, because every proof carries a
jtithat is used once. - If the request also carries a client assertion, build a new assertion for the retry. The first one is already used and the retry would fail with
invalid_client. - A second
use_dpop_noncein a row means the nonce you sent was not accepted. Take the nonce from the latest response, not from a stored copy. - Your own API, if it checks DPoP for you, needs a replay store. Without one you cannot detect a reused proof.
invalid_dpop_proof#
The error_description names the cause. The table lists them.
error_description | Cause | Fix |
|---|---|---|
| the DPoP header is too large | The header value is oversized. | Send only the proof. |
| the DPoP proof is not a decodable JWS | The value is not a compact JWT. | Send the proof as the whole DPoP header value. |
| a DPoP proof must carry typ dpop+jwt | The JWT header typ is wrong. | Set typ to dpop+jwt. |
| the DPoP proof alg is not one this server supports | The algorithm is not allowed. | Use ES256 or EdDSA. Discovery lists them as dpop_signing_alg_values_supported. |
| a DPoP proof must embed its public key as jwk | The header has no jwk. | Put the public key in the header. |
| the DPoP proof key carries private members | The jwk includes the private key. | Export the public part only. Treat the key as exposed and replace it. |
| the DPoP proof signature does not verify | The proof was not signed with the key in its jwk. | Sign with the same key you publish. |
| the DPoP proof htm does not match this request | The htm is not the HTTP method used. | Set htm to the HTTP method of this request. |
| the DPoP proof htu does not match this endpoint | The htu is not the address called. | Use the request address without query or fragment. Check proxies and redirects that change the address. |
| the DPoP proof carries no usable iat | iat is missing or not a number. | Set iat to now, in seconds. |
| the DPoP proof iat is outside the 60-second window | The clock of the caller differs from the server's. The number can be smaller than 60: the platform can shorten the window, never lengthen it. | Fix the clock. See Clock skew. |
| this endpoint presents no access token, so ath must be absent | A proof for the token, pushed authorization or revocation endpoint includes ath. | Leave ath out there. |
| the DPoP proof ath does not match the presented access token | A proof sent with an access token has a missing or wrong ath. | Set ath to the base64url SHA-256 of the access token you send. |
| the DPoP proof carries no usable jti | jti is missing. | Add a fresh jti to every proof. |
| this DPoP proof has already been presented | A proof was reused. | Build a new proof for every request, including a retry. |
| this refresh token is bound to a different key than the proof presented | A refresh token was used with another key. | Use the key the token was issued to. A key that changed at restart loses its refresh tokens. See DPoP. |
| dpop_jkt does not match the key of the DPoP proof | The thumbprint sent with the pushed authorization request differs from the proof key. | Send the thumbprint of the key that signs the proof. |
Related refusals:
invalid_clientwhen the client is not first-party web and no key was proved at the token endpoint. Every other client must use DPoP or certificate binding.invalid_requestwith dpop_jkt is accepted only from a client registered with require_dpop. Removedpop_jktor register the client withrequire_dpop.- At your own API: a
Bearerlabel on a bound token is refused, and so is theDPoPscheme on an unbound one.
What to check#
- Decode the proof and read its header and claims. Compare
htmandhtuwith the request you made. - Compare the proof's
iatwith theDateheader of the response. - Check that the same key is used for the pushed authorization request, the token request and each refresh.
- Check the proof is new for every request and every retry.
Fix#
Use the SDK's proof builder, which sets all of this and handles the nonce. When you build proofs yourself, follow DPoP exactly, and store the key where it outlives a restart.