Skip to content

feat: client-side DPoP nonce caching across calls - #170

Merged
osanderson merged 1 commit into
mainfrom
feat/client-dpop-nonce-cache
Aug 27, 2026
Merged

feat: client-side DPoP nonce caching across calls#170
osanderson merged 1 commit into
mainfrom
feat/client-dpop-nonce-cache

Conversation

@osanderson

Copy link
Copy Markdown
Collaborator

Summary

Closes a gap surfaced while testing the recent DPoP nonce-challenge work: none of client's three nonce-aware call sites (token endpoint, PAR, ResourceClient.Do) carried a server-provided nonce forward to the next, independent call — each only reacted to a challenge within the one call already in flight. An AS/RS that proactively reissues a nonce on every success (which this session's own server/resource work does via NextDPoPNonce) got no benefit from it: every call paid the extra round trip, not just the first.

  • New optional Dependencies.DPoPNonceCache, following the same opt-in shape Decryption and Clock/SystemClock already use — nil disables it entirely (today's exact behavior); a caller opts in explicitly with NewInMemoryDPoPNonceCache() (shipped, mutex-guarded) or their own implementation. It's a hint, not a ledger — a missing or stale cached value just costs one extra challenge/retry round trip, recovered by the existing retry logic exactly as if the cache didn't exist.
  • PAR and the token endpoint share one cache scope, matching how server.Dependencies.Nonces already treats them as one shared nonce space — a nonce obtained from either primes the other, so PAR's own success can prime the very first token request of a flow (confirmed by the new test, better than originally expected). Resource calls are scoped per resource-server origin, since Do can be pointed at any URL and RFC 9449 §9 nonces are per-RS.

Test plan

  • go build ./..., go vet ./..., go test ./... -race all clean
  • New tests: InMemoryDPoPNonceCache round-trip/isolation/concurrency, a full two-flow test proving PAR+token both skip their retry on the second flow once cached, a stale-cache-still-recovers test, and the equivalent for ResourceClient.Do
  • Every existing test (none of which set DPoPNonceCache) passes unchanged — confirms the optimization is fully opt-in
  • Full local OIDF conformance run: all four suites pass with no regressions — expected, since cmd/conformance-client doesn't wire the cache in, so this change is behaviorally invisible unless opted into

…nging

None of the three nonce-aware call sites (the token endpoint, PAR,
ResourceClient.Do) carried a server-provided nonce forward to the next,
independent call — each only reacted to a challenge within the one call
already in flight. So an AS/RS that proactively reissues a nonce on
every success (this session's own server/resource work does exactly
this via NextDPoPNonce) got no benefit: every call paid the extra round
trip, not just the first.

Adds an optional Dependencies.DPoPNonceCache, following the same
opt-in shape Decryption and Clock/SystemClock already use — nil
disables it entirely, exactly today's behavior; a caller opts in
explicitly with NewInMemoryDPoPNonceCache() or their own
implementation. It's a hint, not a ledger: a missing or stale cached
value costs one extra challenge/retry round trip, recovered by the
existing retry logic exactly as if the cache didn't exist.

PAR and the token endpoint share one cache scope, matching how
server's own Dependencies.Nonces already treats them as one shared
nonce space — a nonce obtained from either primes the other, so PAR's
own success can prime the very first token request of a flow.
Resource calls are scoped per resource-server origin, since Do can be
pointed at any URL and RFC 9449 §9 nonces are per-RS.

Verified against the live OIDF conformance suite: unaffected, since
cmd/conformance-client doesn't wire the cache in — this change is
purely additive and behaviorally invisible unless opted into.

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

Copy link
Copy Markdown

@osanderson
osanderson merged commit 5c37f06 into main Aug 27, 2026
8 checks passed
@osanderson
osanderson deleted the feat/client-dpop-nonce-cache branch August 27, 2026 18:04
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