skip to content

An OAuth 2.0 token endpoint rejects a commissioning tool's DPoP proof with 400 and `use_dpop_nonce` — what must the client do next?

level: seniorimportance: should knowfreq 33%

answer

  1. a 400 that is not a failure
  2. the server wants a value it chose
  3. copy the response header into the proof
  4. fresh jti and iat, then retry
  5. the nonce can change at any time

basics

~20 s

Treat it as a challenge, not a failure. The 400 response carries a DPoP-Nonce header; the client signs a fresh proof with a new jti and current iat, includes that value as the nonce claim, and retries the same token request.

solid answer

~40 s

`use_dpop_nonce` means the proof was acceptable in shape but must carry a server-chosen nonce. The 400 response includes a `DPoP-Nonce` header whose value the client copies into the `nonce` claim of a **freshly signed** proof — new `jti`, current `iat`, same `htm` and `htu` — and then repeats the same token request. The server chooses those values and RFC 9449 requires them to be unpredictable, so the client can never precompute one. A server may also supply a new `DPoP-Nonce` at any later point and challenge again when an old one goes stale, so the loop is ordinary traffic rather than an error path. `invalid_dpop_proof` is the different answer: the proof itself failed validation, and retrying it unchanged will fail again.

code

http · 9 lines
http
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store
DPoP-Nonce: eyJ7S_zG.eyJH0-Z.HX4w-7v

{
  "error": "use_dpop_nonce",
  "error_description": "Authorization server requires nonce in DPoP proof"
}

go deeper

for a junior

Recall that this particular 400 is an instruction, not a dead end: the response hands back a value the next attempt has to include, and the same request is then sent again.

for a middle

Explain the exchange precisely: the DPoP-Nonce header supplies the value, the client re-signs a proof with a new jti and current iat carrying it as the nonce claim, and repeats the token request.

for a senior

Demonstrate that you would store the latest nonce per authorization server, bound the retry, and separate this challenge from invalid_dpop_proof before touching anything, since one is a retry and the other a defect in the proof.

for a principal

Consider the fleet question: enabling nonce enforcement breaks every client that never implemented the retry at the same instant, so it belongs behind a staged rollout with client readiness measured first.

## The situation A rooftop-solar installer's commissioning tool runs on an engineer's laptop and holds its own key pair, so the authorization server can bind the access token it issues to that key. The tool worked for months. Today, at a site with no signal to spare, its very first token request comes back `400 Bad Request` with `{"error":"use_dpop_nonce"}` — and the tool, which treats any 4xx as fatal, gives up and asks the engineer to sign in again, forever. Nothing is broken. The server has started demanding a nonce, and the client never learned the retry. ## What the 400 actually says `use_dpop_nonce` is a **challenge**. It says: your proof was well-formed, but I will not accept one whose freshness rests solely on a timestamp you chose. Include a value that **I** chose. The value arrives in the `DPoP-Nonce` response header of that same 400. RFC 9449 requires the server's nonce values to be **unpredictable**, which is the point: a proof carrying one demonstrates that the signer was in live contact with this server, rather than replaying something signed earlier or signed with a clock that has drifted. ## The retry, step by step 1. Read the `DPoP-Nonce` header from the 400 response and store it against that server. 2. Sign a **new** proof — do not patch the old one. Keep the `typ` of `dpop+jwt` and the public key in the `jwk` header, keep `htm` and `htu` matching the token request, generate a fresh `jti` and set `iat` to now. 3. Add the `nonce` claim with the value from the header. 4. Repeat the same token request with the new proof. Re-signing rather than patching matters: `jti` is there so the server can reject a repeat, and a stale `iat` can fall outside the window the server accepts. ## Nonces are not issued once - The server may hand out a **new** `DPoP-Nonce` at any time, including on a successful response, and the client should adopt the latest value it has seen. - A nonce that has gone stale draws the same `use_dpop_nonce` challenge again. That is routine, not a regression. - The client should retry **once per nonce it is given**, not in an unbounded loop. A tool that retries blindly turns a server-side change into a hot loop against the token endpoint. - The client cannot invent or predict a nonce. Nothing derived locally, and no value carried over from another server, will be accepted. ## Two error codes that look alike and are not | | `use_dpop_nonce` | `invalid_dpop_proof` | |---|---|---| | what it says about the proof | acceptable, but missing or carrying a stale server nonce | it failed validation | | typical cause | the server requires nonces and this proof has none | bad signature, wrong `htm` or `htu`, an `iat` outside the accepted window, a replayed `jti`, an algorithm the server does not accept | | correct client action | re-sign with the supplied nonce and retry | fix the proof; a retry of the same bytes fails again | | carries a `DPoP-Nonce` header | yes | not as part of the meaning | Collapsing the two is the most expensive mistake here. A client that retries an `invalid_dpop_proof` unchanged hammers the endpoint and learns nothing; a client that gives up on `use_dpop_nonce` cannot obtain a token at all. ## Related metadata worth checking first Before blaming the nonce, check what the authorization server says it accepts. Its metadata document lists `dpop_signing_alg_values_supported` — the JWS algorithms it will take for a proof. A proof signed with an unlisted algorithm is rejected as an invalid proof, not answered with a nonce challenge, so a client that changed its key type and now sees a flat rejection should look here rather than at the nonce loop. ## Why this is a production failure and not a tutorial detail Nonce enforcement is a server-side switch. A deployment can run for a year without it, and turn it on for a good reason — replay pressure, clock drift across a fleet of laptops that sleep for days — without changing anything the client can see beforehand. Every client that never implemented the retry fails at exactly the same moment, and the symptom reported is 'sign-in is broken', with a 400 in the log that reads like a client bug. The fix is not a configuration rollback; it is the four steps above, plus storing the latest nonce per authorization server so the second attempt is not a fresh challenge too.

  • Which claims does a DPoP proof carry besides the nonce?
    The proof is a JWT with `typ` of `dpop+jwt` and the public key in its `jwk` header. Its payload carries `htm`, the request method; `htu`, the request URI without query or fragment; `iat`; and a unique `jti`. An `ath` claim, the hash of an access token, is present where the proof accompanies one.
  • How does `use_dpop_nonce` differ from `invalid_dpop_proof`?
    `use_dpop_nonce` says the proof was otherwise acceptable but lacked a current server-supplied nonce; the client re-signs with the value from `DPoP-Nonce` and retries. `invalid_dpop_proof` says validation failed — signature, `htm` or `htu` mismatch, an `iat` outside the window, or a replayed `jti` — and retrying the same proof fails again.
  • How does a client know which algorithms it may sign a DPoP proof with?
    From `dpop_signing_alg_values_supported` in the authorization server's metadata document, which lists the JWS algorithms the server accepts for proofs. Signing with one that is not listed draws an invalid-proof rejection rather than a nonce challenge, which is a useful way to tell the two apart.
  • Why must the server's nonce values be unpredictable?
    Because a nonce the client could guess or derive would prove nothing about when the proof was signed. Requiring an unpredictable, server-chosen value makes a proof evidence of live contact with that server, which is what closes the gap left by trusting the client's own `iat`.

saying these in an interview costs you the question

  • Treats the 400 as fatal and abandons the flow
  • Reuses the same jti and iat when retrying with the nonce
  • Confuses this nonce with the OpenID Connect authorization-request nonce
  • Assumes a nonce is issued once and stays valid indefinitely
  • Says the client chooses the nonce value and the server echoes it
  • Retries an invalid_dpop_proof rejection unchanged