feat: DPoP nonce-challenge support for PAR and the token endpoint - #168
Merged
Conversation
server never implemented the AS half of RFC 9449 §8's nonce challenge — only client consumed one, from a third-party AS. Adds it to both PAR and the token endpoint, sharing one nonce store (Dependencies.Nonces): a nonce issued from a PAR challenge is valid at the token endpoint and vice versa, the same way resource.Dependencies.Nonces already covers every protected-resource endpoint uniformly. Genuinely optional, like its resource-package sibling — nil disables it entirely, no visible-opt-out sentinel needed the way Revocation's is, since RFC 9449 §8 makes this a MAY, not a security check this module treats as non-negotiable. The check only ever runs inside PAR's existing "a proof was presented" branch (reconcileParDPoPBinding) — PAR's own DPoP optionality is untouched, nonce-challenge never makes DPoP mandatory there. A successful PAR or token call proactively issues another nonce (TokenResult.NextDPoPNonce, PushAuthorizationResult.NextDPoPNonce) so steady-state traffic only pays the extra round trip once, at the very first request. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ndpoints Extends the existing -dpop-nonce-challenge flag (still off by default) to also cover this binary's own PAR and token endpoints, reusing a separate memstore.NonceStore from the resource-side one wired earlier — PAR/token (this server's own role, RFC 9449 §8) and /accounts, /userinfo (the resource role, §9) are logically distinct nonce spaces even though this one demo binary happens to host both. DPoP-Nonce is set from Error.Nonce on a use_dpop_nonce rejection (both endpoints share writeOAuthJSONError) and from NextDPoPNonce on a successful PAR or token response. TestSmokeUserInfoWithDPoPNonceChallenge now transparently exercises the token-endpoint challenge too, since CompleteAuthorization's internal exchange goes through it — client.ExchangeCode's existing retry logic handles it with no test changes needed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Adds the AS-side half of RFC 9449 §8's DPoP nonce challenge — the sibling of PR #166 (resource-server side, §9). Neither
server's PAR nor token endpoint implemented this before; onlyclientconsumed a challenge from a third-party AS.server:Dependencies.Nonces(one sharedstorage.NonceStorefor everything this server verifies) +Limits.DPoPNonceLifetime, both genuinely optional (nil disables entirely — same "not a security check this module treats as non-negotiable" reasoning asresource.Dependencies.Nonces, since RFC 9449 §8 is a MAY).verifyTokenRequestDPoP(shared chokepoint forExchangeAuthorizationCode/RefreshAccessToken) andreconcileParDPoPBindingboth run the same nonce check right after their base DPoP proof verification. A nonce issued from a PAR challenge is valid at the token endpoint and vice versa.TokenResult/PushAuthorizationResultboth gainNextDPoPNonce, proactively issued on every success so steady-state traffic only pays the extra round trip once.client, today) is completely unaffected even with nonce-challenge enabled.cmd/conformance-as: extends the existing-dpop-nonce-challengeflag to also cover PAR/token, using a separate nonce store from the resource-side one (logically distinct roles, even though this one demo binary hosts both). Still off by default.Test plan
go build ./...,go vet ./...,go test ./...all cleanserverunit tests: full nonce-challenge lifecycle forExchangeAuthorizationCode(missing/unknown/expired/valid nonce, single-use enforcement, checked-before-code-redemption, proactive reissue), a lighter check forRefreshAccessToken, PAR coverage including the key regression case (a PAR call with no DPoP proof is completely unaffected), and cross-endpoint nonce sharing (issued at PAR, valid at the token endpoint) — plus config validationcmd/conformance-as.TestSmokeUserInfoWithDPoPNonceChallengenow transparently exercises the token-endpoint challenge too (viaCompleteAuthorization's internal exchange), with zero test changes needed —client.ExchangeCode's existing retry logic already handles itconformance/scripts/run-all.sh) with the flag left at its default (off): AS baseline (57 modules, 4465 conditions, 0 failures), AS message-signing (71 modules, 5975 conditions, 0 failures), RP baseline (22/22 PASSED), RP message-signing (28/28 PASSED) — no regressions