Skip to content
TDACorpIDENTITYSign in

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 jti that 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_nonce in 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_descriptionCauseFix
the DPoP header is too largeThe header value is oversized.Send only the proof.
the DPoP proof is not a decodable JWSThe value is not a compact JWT.Send the proof as the whole DPoP header value.
a DPoP proof must carry typ dpop+jwtThe JWT header typ is wrong.Set typ to dpop+jwt.
the DPoP proof alg is not one this server supportsThe 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 jwkThe header has no jwk.Put the public key in the header.
the DPoP proof key carries private membersThe jwk includes the private key.Export the public part only. Treat the key as exposed and replace it.
the DPoP proof signature does not verifyThe 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 requestThe htm is not the HTTP method used.Set htm to the HTTP method of this request.
the DPoP proof htu does not match this endpointThe 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 iatiat is missing or not a number.Set iat to now, in seconds.
the DPoP proof iat is outside the 60-second windowThe 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 absentA 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 tokenA 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 jtijti is missing.Add a fresh jti to every proof.
this DPoP proof has already been presentedA 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 presentedA 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 proofThe 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_client when 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_request with dpop_jkt is accepted only from a client registered with require_dpop. Remove dpop_jkt or register the client with require_dpop.
  • At your own API: a Bearer label on a bound token is refused, and so is the DPoP scheme on an unbound one.

What to check#

  1. Decode the proof and read its header and claims. Compare htm and htu with the request you made.
  2. Compare the proof's iat with the Date header of the response.
  3. Check that the same key is used for the pushed authorization request, the token request and each refresh.
  4. 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.

Verified against commit ca46b57