Skip to content

feat: DPoP nonce-challenge support for the resource server - #166

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

feat: DPoP nonce-challenge support for the resource server#166
osanderson merged 3 commits into
mainfrom
feat/dpop-nonce-challenge

Conversation

@osanderson

Copy link
Copy Markdown
Collaborator

Summary

Adds resource-server DPoP nonce-challenge support (RFC 9449 §8/§9), which no role in this module implemented before — only client consumed one, from a third-party AS. Scoped to the resource package (used to protect UserInfo or any other resource endpoint an AS hosts), not server's token endpoint.

  • internal/dpop: VerifiedProof now surfaces the proof's own nonce claim unconditionally (needed since single-use, issued-per-challenge nonces can't use RequiredNonce's compare-to-one-known-value check — the verifier has to look up whether the presented value was actually issued).
  • storage: new NonceStore contract + storage/memstore reference implementation, mirroring SessionStore/ReplayStore's issue-once/consume-once shape and contract-test pattern exactly.
  • resource: Verifier gains full nonce-challenge behavior — genuinely optional (Dependencies.Nonces nil disables it entirely, zero behavior change from today, no visible-opt-out sentinel needed the way Revocation's is, since RFC 9449 §8 makes this a resource-server MAY, not a security check this module treats as non-negotiable). A request whose proof carries no current, unconsumed nonce is rejected with a new ErrorUseDPoPNonce code and a freshly issued replacement (Error.Nonce()); a successful Verify proactively issues another one (AuthorizationContext.NextDPoPNonce) so steady-state traffic only pays the extra round trip once.
  • cmd/conformance-as: new -dpop-nonce-challenge flag (off by default — the OIDF suite's own protected-resource caller isn't guaranteed to retry on the challenge) wires this end to end on the example /accounts//userinfo endpoints.

Test plan

  • go build ./..., go vet ./..., go test ./... all clean
  • New unit tests: storage.TestNonceStoreContract (+ memstore), internal/dpop.TestVerifyReturnsProofNonce, resource's nonce-challenge suite (missing/unknown/expired/valid nonce, single-use enforcement, proactive reissue, config validation)
  • cmd/conformance-as.TestSmokeUserInfoWithDPoPNonceChallenge — full end-to-end integration test proving client's existing nonce-retry logic (ResourceClient.Do) interoperates correctly with the new server-side challenge, entirely through the public FetchUserInfo API
  • Full local OIDF conformance run (conformance/scripts/run-all.sh) with the new 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 3 commits August 28, 2026 00:17
Foundational pieces for DPoP nonce-challenge support (RFC 9449 §8, §9):
a single-use NonceStore contract (storage package) plus its in-memory
reference implementation, mirroring SessionStore/ReplayStore's own
issue-once/consume-once shape exactly. internal/dpop.VerifiedProof now
also surfaces the proof's own "nonce" claim unconditionally — unlike
RequiredNonce's compare-to-one-known-value check, a caller implementing
single-use, issued-per-challenge nonces doesn't know the expected value
in advance and has to look up whether the presented one was actually
issued, which only that caller can do.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neither this module's resource role nor its server role has ever
implemented the resource-server half of RFC 9449 §8/§9's nonce
challenge — only client consumes one, from a third-party AS. Adds it to
resource.Verifier as genuinely optional (Dependencies.Nonces nil
disables it entirely, same as today): a request whose DPoP proof
carries no current, unconsumed nonce is rejected with the new
ErrorUseDPoPNonce code and a freshly issued replacement attached (via
Error.Nonce); a successful Verify proactively issues another one
(AuthorizationContext.NextDPoPNonce) so steady-state traffic only pays
the extra round trip once, at the very first request.

Deliberately not a required-with-visible-opt-out dependency like
Revocation: RFC 9449 §8 makes this a resource-server MAY, and declining
it is the normal, fully spec-compliant default, not a security check
this module considers non-negotiable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New -dpop-nonce-challenge flag (off by default) demonstrates
resource.Verifier's new nonce-challenge support end to end on the
/accounts and /userinfo example endpoints: DPoP-Nonce is set from
Error.Nonce on a use_dpop_nonce rejection and from
AuthorizationContext.NextDPoPNonce on success. Off by default because
the OIDF suite's own AS-plan protected-resource caller isn't guaranteed
to implement the client-side retry the way this module's own client
package does — flipping it on unconditionally would risk breaking
unrelated AS conformance.

TestSmokeUserInfoWithDPoPNonceChallenge confirms the two independently
built halves actually interoperate: FetchUserInfo succeeds
transparently against a nonce-challenging endpoint, entirely through
client's existing ResourceClient.Do retry logic, with no special
handling in the test itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
59.5% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@osanderson
osanderson merged commit b2c3bf8 into main Aug 27, 2026
7 of 8 checks passed
@osanderson
osanderson deleted the feat/dpop-nonce-challenge branch August 27, 2026 16:21
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