feat(sdk): add DPoP client support with HTTP RoundTripper (DSPX-3397)#3581
feat(sdk): add DPoP client support with HTTP RoundTripper (DSPX-3397)#3581dmihalcik-virtru wants to merge 3 commits into
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request implements RFC 9449 DPoP (Demonstrating Proof-of-Possession) client support for the OpenTDF Go SDK. By introducing a custom HTTP RoundTripper, the SDK can now generate and attach DPoP proofs to HTTP requests, handle server-side nonce challenges, and perform URI normalization. This work is a key component of the broader Keycloak v26 upgrade, ensuring secure, proof-of-possession-based authentication for HTTP-based interactions within the platform. Highlights
New Features🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. The proof is shown in token light, With DPoP we do it right. No replay here, the nonce is set, A secure path for the internet. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces RFC 9449 DPoP (Demonstrating Proof-of-Possession) support to the SDK by adding a new DPoPTransport and integrating it into the client setup. The code review identified several critical and high-severity issues in the transport implementation, including a potential bug where request bodies are consumed and not reset on retry, concurrency data races on shared fields like t.Base and t.nonceCache, and the bypass of custom transport configurations when retrieving access tokens. Additionally, optimizations were suggested to cache parsed token endpoint URLs and normalize URL origins to lowercase to prevent cache misses.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if t.Base == nil { | ||
| t.Base = http.DefaultTransport | ||
| } | ||
|
|
||
| if t.nonceCache == nil { | ||
| t.nonceMu.Lock() | ||
| if t.nonceCache == nil { | ||
| t.nonceCache = make(map[string]string) | ||
| } | ||
| t.nonceMu.Unlock() | ||
| } |
There was a problem hiding this comment.
Concurrency Data Race on t.Base and t.nonceCache
Concurrently modifying t.Base and performing a double-checked lock read on t.nonceCache without synchronization can lead to data races when multiple goroutines use the same DPoPTransport instance.
To resolve this:
- Use a local variable
baseinstead of modifying the struct fieldt.Base. - Perform the initialization of
t.nonceCacheunder an unconditional lock to avoid the data race on the initial read.
base := t.Base
if base == nil {
base = http.DefaultTransport
}
t.nonceMu.Lock()
if t.nonceCache == nil {
t.nonceCache = make(map[string]string)
}
t.nonceMu.Unlock()| nonce := t.getCachedNonce(origin) | ||
|
|
||
| // Generate and add DPoP proof | ||
| if err := t.addDPoPProof(req2, nonce, isTokenRequest); err != nil { |
| } | ||
|
|
||
| // Make the request | ||
| resp, err := t.Base.RoundTrip(req2) |
| req3 := cloneRequest(req) | ||
|
|
||
| // Regenerate proof with nonce | ||
| if err := t.addDPoPProof(req3, newNonce, isTokenRequest); err != nil { |
There was a problem hiding this comment.
| } | ||
|
|
||
| // addDPoPProof generates and adds DPoP proof to the request headers. | ||
| func (t *DPoPTransport) addDPoPProof(req *http.Request, nonce string, isTokenRequest bool) error { |
There was a problem hiding this comment.
Update the signature of addDPoPProof to accept the base transport.
| func (t *DPoPTransport) addDPoPProof(req *http.Request, nonce string, isTokenRequest bool) error { | |
| func (t *DPoPTransport) addDPoPProof(req *http.Request, base http.RoundTripper, nonce string, isTokenRequest bool) error { |
| nonceMu sync.RWMutex | ||
| // nonceCache stores server-issued nonces by origin (scheme://host:port) | ||
| nonceCache map[string]string |
There was a problem hiding this comment.
Optimize Token Endpoint URL Parsing (Struct Fields)
Add cached fields for the parsed token endpoint URL to avoid parsing it on every request.
nonceMu sync.RWMutex
// nonceCache stores server-issued nonces by origin (scheme://host:port)
nonceCache map[string]string
cachedTokenURL *url.URL
cachedTokenURLStr string
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
441af7b to
61316ef
Compare
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
Implements RFC 9449 DPoP (Demonstrating Proof-of-Possession) for the Go SDK: - Add DPoPTransport as an http.RoundTripper that wraps any transport - Generate ES256/RS256 proofs with jti, htm, htu, iat claims for all requests - Add ath claim (access token hash) for resource endpoint calls - Handle server-issued DPoP-Nonce challenges with automatic retry - Cache nonces per-origin and refresh from successful responses - Normalize URIs per RFC 9449 (lowercase scheme/host, strip default ports) - Integrate into SDK's HTTP client construction via NewDPoPHTTPClient - Add SupportedFeatures() function for xtest feature detection All requests through the SDK now include DPoP proofs when credentials are configured. Token endpoint requests omit the ath claim; resource requests include both Authorization: DPoP <token> header and the DPoP proof header. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…PX-3397) Exposes DPoP algorithm/key selection via CLI flags on `otdfctl encrypt` and `otdfctl decrypt`, supporting ES256 (default), ES384, ES512, RS256, RS384, and RS512. Bare `--dpop` defaults to ES256 per RFC 9449 §4.2. `--dpop-key <path>` loads a PEM private key (algorithm inferred from key type). Both flags can be combined to override the inferred algorithm. SDK changes: - Add sdk/dpop_key.go: generateDPoPKeyForAlg, loadDPoPKeyFromPEM, resolveDPoPKey helpers - Add WithDPoPAlgorithm, WithDPoPKeyPEM, WithDPoPJWK SDK options - Thread custom JWK through buildIDPTokenSource and DPoPTransport setup; falls back to auto-generated RSA when no custom key is configured - Add JWK-accepting token source constructors for all four source types otdfctl changes: - Register --dpop (NoOptDefVal="ES256") and --dpop-key flags on encrypt/decrypt; update man docs accordingly - handlers.WithExtraSDKOpts appends (not replaces) SDK options - common.NewHandler accepts variadic extraSDKOpts (backward compatible) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
Fixes critical and high-priority issues identified in PR review: - Fix request body consumed on retry: reset body using GetBody() before retrying - Fix data races: use local base variable instead of modifying t.Base - Fix nonce cache initialization: unconditional lock instead of double-checked lock - Fix missing HTTP client for token source: pass client with base transport to preserve custom configs - Optimize token endpoint URL parsing: cache parsed URL to avoid parsing on every request - Normalize origin casing: lowercase origin in cache to ensure consistent hits on uppercase URLs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ebc3e40 to
37ed377
Compare
|
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
Summary
Implements RFC 9449 DPoP (Demonstrating Proof-of-Possession) client support for the OpenTDF Go SDK.
This PR is part of the larger Keycloak v26 upgrade and comprehensive DPoP support feature tracked in DSPX-3397.
Changes
DPoP RoundTripper Implementation
sdk/auth/dpop_transport.go: NewDPoPTransportthat implementshttp.RoundTripperjti,htm,htu,iat(always);ath(resource calls only);nonce(when challenged)Server-Issued Nonce Support
DPoP-Noncechallenges per RFC 9449 §8401withDPoP-Nonceheader: cache nonce, regenerate proof, retry once2xxresponsesSDK Integration
sdk/sdk.go: Wrap HTTP client with DPoP transport during SDK constructiongetDPoPJWK()helper to convertocrypto.RsaKeyPairtojwk.KeyNewDPoPHTTPClient()factory for wrapping clients with DPoP supportFeature Detection
sdk/version.go: AddSupportedFeatures()function returning["dpop", "connectrpc"]supports dpopprobeTesting
sdk/auth/dpop_transport_test.go: Comprehensive unit testsath) verificationRelated Work
This PR implements the Go SDK cell of the DPoP feature. Related PRs:
xtest/scenarios/DSPX-3397.yaml)Testing
All tests pass:
Linting clean:
Notes
oauth.goalready handles DPoP for token endpoint requeststoken_adding_interceptor.goalready handles DPoP for gRPC/Connecthttp.ClientJira: DSPX-3397
Test Scenario:
xtest/scenarios/DSPX-3397.yaml