feat(oauth): DPoP resource-server validation in the client library (#118)#148
Merged
Conversation
) Third DPoP slice (RFC 9449): a resource server using Modgud.Client.AspNetCore's reference-token (introspection) mode now validates the DPoP binding end to end. Builds on issuance (#147). - The DPoP crypto core (JwkThumbprint + DpopProofValidator + DpopValidationResult) is duplicated verbatim into the client library (BCL-only), keeping this published NuGet dependency-light rather than referencing server internals. Each copy carries a keep-in-sync banner. - DpopResourceValidator: validates the request's DPoP proof against the method + URL + access-token hash (ath) and confirms the proof key's thumbprint equals the token's cnf.jkt (RFC 9449 §7.2). Pure/stateless. - ModgudIntrospectionHandler now accepts the `Authorization: DPoP` scheme, surfaces cnf.jkt out of the introspection response, and enforces the binding: a DPoP-bound token MUST be presented with the DPoP scheme + a matching proof; a bound token used as a plain bearer token is rejected, and a DPoP request against an unbound token is rejected. Verified that OpenIddict's stock introspection already echoes `cnf` for a DPoP-bound reference token (integration test), so no AS-side handler is needed — the RS reads it straight from the standard response. Tests: 7 client-lib unit tests (cnf surfacing + proof accept / no-proof / wrong-url / ath-mismatch / key-mismatch) and an AS integration test pinning the introspection cnf echo. Full DPoP suite green (50 unit + 5 integration). Follow-ups: JWT-bearer-path RS validation (AddModgudClient); discovery (dpop_signing_alg_values_supported) + nonce + refresh binding + per-client RequireDpop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 19, 2026
windischb
added a commit
that referenced
this pull request
Jul 19, 2026
#151) PR #148 enforced the DPoP cnf.jkt binding resource-server-side only for opaque reference tokens (the introspection path). A DPoP-bound *JWT* was still honoured as a plain bearer token — the sender constraint was never checked. This closes that gap for the JwtBearer validation path, the JWT twin of ModgudIntrospectionHandler. Two composable hooks added in ModgudJwtBearerPostConfigure: - OnMessageReceived — a DPoP-bound token is presented under the `DPoP` auth scheme, which JwtBearer's `Bearer`-only extraction ignores. Lift the token out of the `DPoP` header so the JWT actually gets validated. Plain `Bearer` requests are untouched; the host's own handler is preserved. - OnTokenValidated — after validation, enforce the binding (RFC 9449 §7.1): a token carrying cnf.jkt MUST arrive under the DPoP scheme with a valid proof whose key thumbprint matches; a bound token presented as bearer, or the DPoP scheme against an unbound token, is rejected (401). Runs before UserInfo enrichment — no point enriching a request we're about to fail. The proof cryptography reuses the shared DpopResourceValidator / DpopProofValidator core (identical to the introspection path); only the JWT-specific glue is new. No change to the duplicated crypto files, so no keep-in-sync impact. Tests (Modgud.Tests.Unit): the pure decision core (EvaluateBinding) is pinned across every branch — unbound/bearer ok, unbound/DPoP rejected, bound/bearer rejected, bound/DPoP with valid/missing/wrong-url/wrong-ath/ wrong-key proof — plus ExtractDpopSchemeToken, TryGetBoundJkt, and the OnTokenValidated→Fail wiring exercised on a real TokenValidatedContext. Full DPoP unit suite green (73). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 19, 2026
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.
Part of #118. Third DPoP (RFC 9449) slice, on top of issuance (#147).
A resource server using
Modgud.Client.AspNetCore's reference-token(introspection) mode now validates the DPoP binding end to end.
Changes
Dpop/JwkThumbprint,DpopProofValidator,DpopValidationResult) — verbatim, BCL-only, eachwith a keep-in-sync banner. Keeps this published NuGet dependency-light
instead of referencing server internals (the agreed decision).
DpopResourceValidator— validates the request'sDPoPproofagainst the HTTP method + URL + access-token hash (
ath) and confirmsthe proof key's RFC 7638 thumbprint equals the token's
cnf.jkt(RFC 9449 §7.2). Pure and stateless.
ModgudIntrospectionHandlernow accepts theAuthorization: DPoPscheme, surfaces
cnf.jktfrom the introspection response, and enforcesthe binding:
cnf.jktpresent) → must be presented with theDPoP scheme and a matching, valid proof;
Bearer→ rejected;Notable finding
OpenIddict's stock introspection already echoes
cnffor a DPoP-boundreference token (pinned by an integration test), so no AS-side echo handler
is needed — the RS reads it straight from the standard RFC 7662 response.
Tests
7 client-lib unit tests (cnf surfacing; proof accept / no-proof / wrong-url
/ ath-mismatch / key-mismatch) + an AS integration test pinning the
introspection
cnfecho. Full DPoP suite green (50 unit + 5 integration).Follow-ups (this is 3 of ~4)
AddModgudClient/OnMessageReceived+OnTokenValidated) — reference-token path lands here; JWT path is next.dpop_signing_alg_values_supported) + nonce + refreshbinding + per-client
RequireDpop.jtireplay cache.🤖 Generated with Claude Code