Skip to content

feat: DPoP nonce-challenge support for PAR and the token endpoint - #168

Merged
osanderson merged 2 commits into
mainfrom
feat/dpop-nonce-challenge-server
Aug 27, 2026
Merged

feat: DPoP nonce-challenge support for PAR and the token endpoint#168
osanderson merged 2 commits into
mainfrom
feat/dpop-nonce-challenge-server

Conversation

@osanderson

Copy link
Copy Markdown
Collaborator

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; only client consumed a challenge from a third-party AS.

  • server: Dependencies.Nonces (one shared storage.NonceStore for everything this server verifies) + Limits.DPoPNonceLifetime, both genuinely optional (nil disables entirely — same "not a security check this module treats as non-negotiable" reasoning as resource.Dependencies.Nonces, since RFC 9449 §8 is a MAY). verifyTokenRequestDPoP (shared chokepoint for ExchangeAuthorizationCode/RefreshAccessToken) and reconcileParDPoPBinding both 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/PushAuthorizationResult both gain NextDPoPNonce, proactively issued on every success so steady-state traffic only pays the extra round trip once.
  • PAR's own DPoP optionality is untouched — the nonce check only runs inside the existing "a proof was presented" branch, so a client that never sends a proof at PAR (like FAPIgo's own client, today) is completely unaffected even with nonce-challenge enabled.
  • cmd/conformance-as: extends the existing -dpop-nonce-challenge flag 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 clean
  • New server unit tests: full nonce-challenge lifecycle for ExchangeAuthorizationCode (missing/unknown/expired/valid nonce, single-use enforcement, checked-before-code-redemption, proactive reissue), a lighter check for RefreshAccessToken, 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 validation
  • cmd/conformance-as.TestSmokeUserInfoWithDPoPNonceChallenge now transparently exercises the token-endpoint challenge too (via CompleteAuthorization's internal exchange), with zero test changes needed — client.ExchangeCode's existing retry logic already handles it
  • Full local OIDF conformance run (conformance/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

osanderson and others added 2 commits August 28, 2026 01:10
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>
@sonarqubecloud

Copy link
Copy Markdown

@osanderson
osanderson merged commit 88b45df into main Aug 27, 2026
8 checks passed
@osanderson
osanderson deleted the feat/dpop-nonce-challenge-server branch August 27, 2026 17:13
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