Skip to content

fix(server): check the DPoP nonce before consuming the request object - #825

Merged
osanderson merged 4 commits into
mainfrom
fix/dpop-nonce-before-request-object
Nov 10, 2026
Merged

osanderson merged 4 commits into
mainfrom
fix/dpop-nonce-before-request-object

Conversation

@osanderson

Copy link
Copy Markdown
Collaborator

Summary

With Dependencies.Nonces set, PushAuthorizationRequest and BeginBackchannelAuthentication verified the request object (or signed authentication request), recording its jti as used, before verifying the DPoP proof and running the nonce check. A first request without a nonce was refused with use_dpop_nonce after the request object had already been consumed, so the client's retry of the same request with a nonce-bearing proof was refused as a replay (invalid_request_object at PAR, invalid_request at CIBA). RFC 9449 §8 asks the client to retry with the nonce; nothing in RFC 9101 or FAPI 2.0 Message Signing asks it to re-sign the request object. Found by the openid-client interop suite (#818), whose client resends the same signed request object on its nonce retry.

  • Both endpoints now verify the optional DPoP proof (its own jti replay record and the nonce) right after client authentication, before the request object, via a shared verifyOptionalDPoPProof.
  • reconcileParDPoPBinding / reconcileBackchannelDPoPBinding keep the dpop_jkt match / implicit dpop_jkt logic in the same place, taking the verified proof's thumbprint.
  • Error codes and messages are unchanged. One visible ordering change: a request with both an invalid DPoP proof and an invalid request object now reports the DPoP error.
  • The srv-ciba-dpop-nonce mutant moves to the shared helper in server/par.go and now also runs the PAR nonce test.
  • Interop: the Message Signing instance now runs with nonces on (the -dpop-nonce-challenge=false workaround and its README "Known disagreements" entry are removed).

Not changed: the client assertion (private_key_jwt) and the client attestation PoP are still verified and their jtis recorded in client authentication, before any nonce check, at every endpoint. RFC 7523 §3 lets a server accept a jti only once and clients sign a fresh assertion per request (openid-client does), so a client resending the same assertion on its nonce retry is the client's problem. The token endpoints (authorization code, refresh, CIBA token, client credentials) already check the DPoP nonce before redeeming any code, refresh token or auth_req_id.

Tests

  • TestPushAuthorizationRequestNonceChallengeKeepsRequestObjectUsable: PAR with a request object and a nonce-less DPoP proof gets use_dpop_nonce; the same request object with a fresh proof carrying the nonce succeeds; a second successful use is still refused as invalid_request_object.
  • TestBackchannelAuthenticationNonceChallengeKeepsRequestUsable: the same shape for a signed CIBA authentication request.
  • Both fail against the previous server code and pass with the fix.
  • interop/run-openid-client.sh locally: 6/6 legs pass with nonces on for Message Signing; leg 3 fails (invalid_request_object) against the previous server code.
  • go test ./..., go vet ./..., golangci-lint run ./... (Go 1.26.10), sqlstore tests (SQLite), mutation check for srv-ciba-dpop-nonce.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.30435% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
server/par.go 89.47% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@osanderson
osanderson force-pushed the fix/dpop-nonce-before-request-object branch from 8b76871 to 445d2c6 Compare November 10, 2026 17:01
osanderson and others added 4 commits November 11, 2026 01:19
PushAuthorizationRequest and BeginBackchannelAuthentication verified
the request object (or signed authentication request), recording its
jti as used, before verifying the request's DPoP proof and running the
nonce check. With Dependencies.Nonces set, a first request without a
nonce was refused with use_dpop_nonce after its request object had
already been consumed, so the client's retry of the same request with
a nonce-bearing proof (RFC 9449 §8 asks it to retry with the nonce, not
to re-sign) was refused as a request object replay.

Both endpoints now verify the optional DPoP proof, including its own
jti replay record and the nonce, right after client authentication,
in a shared verifyOptionalDPoPProof. The dpop_jkt binding steps stay
where they were and take the verified proof's thumbprint. Error codes
and messages are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Message Signing conformance-as instance ran with
-dpop-nonce-challenge=false because the server consumed a pushed
request object before its DPoP nonce challenge, so openid-client's
nonce retry of the same signed request was refused as a replay. With
the server now checking the nonce first, leg 3 runs with nonces on and
the README's known-disagreement entry for it is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
validateAuthorizationParameters' cognitive complexity was just over
the limit; the PKCE checks move into validatePKCEParameters unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
validateAuthorizationParameters was still just over the cognitive
complexity limit; the response_type checks move into
validateResponseType unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@osanderson
osanderson force-pushed the fix/dpop-nonce-before-request-object branch from 445d2c6 to 847009d Compare November 10, 2026 17:19
@sonarqubecloud

Copy link
Copy Markdown

@osanderson
osanderson merged commit b1b595b into main Nov 10, 2026
30 of 31 checks passed
@osanderson
osanderson deleted the fix/dpop-nonce-before-request-object branch November 10, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant