Skip to content

feat(sdk): add DPoP client support with HTTP RoundTripper (DSPX-3397)#3581

Open
dmihalcik-virtru wants to merge 24 commits into
mainfrom
DSPX-3397-platform-go-sdk
Open

feat(sdk): add DPoP client support with HTTP RoundTripper (DSPX-3397)#3581
dmihalcik-virtru wants to merge 24 commits into
mainfrom
DSPX-3397-platform-go-sdk

Conversation

@dmihalcik-virtru

@dmihalcik-virtru dmihalcik-virtru commented Jun 8, 2026

Copy link
Copy Markdown
Member

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.

e2e Test

Changes

DPoP RoundTripper Implementation

  • sdk/auth/dpop_transport.go: New DPoPTransport that implements http.RoundTripper
    • Wraps any underlying transport (composable with existing HTTP clients)
    • Generates DPoP proofs for both token endpoint and resource requests
    • Proof claims: jti, htm, htu, iat (always); ath (resource calls only); nonce (when challenged)
    • URI normalization per RFC 9449 (lowercase scheme/host, strip default ports, no query/fragment)

Server-Issued Nonce Support

  • Handles DPoP-Nonce challenges per RFC 9449 §8
  • On 401 with DPoP-Nonce header: cache nonce, regenerate proof, retry once
  • Refresh cached nonces from successful 2xx responses
  • Per-origin nonce cache with thread-safe access

SDK Integration

  • sdk/sdk.go: Wrap HTTP client with DPoP transport during SDK construction
  • New getDPoPJWK() helper to convert ocrypto.RsaKeyPair to jwk.Key
  • NewDPoPHTTPClient() factory for wrapping clients with DPoP support
  • Automatically uses ephemeral EC P-256 key (ES256) when no key provided

Feature Detection

  • sdk/version.go: Add SupportedFeatures() function returning ["dpop", "connectrpc"]
  • Enables xtest integration harness to detect DPoP capability via supports dpop probe

Testing

  • sdk/auth/dpop_transport_test.go: Comprehensive unit tests
    • DPoP proof generation and validation
    • Nonce challenge/retry flow
    • URI normalization edge cases
    • Token endpoint vs resource endpoint behavior
    • Access token hash (ath) verification

Related Work

This PR implements the Go SDK cell of the DPoP feature. Related PRs:

  • tests: xtest integration tests and KC26 upgrade (see xtest/scenarios/DSPX-3397.yaml)
  • platform-service: Server-side DPoP validation middleware
  • java-sdk: Java SDK DPoP client support
  • web-sdk: Web SDK DPoP integration verification

Testing

All tests pass:

go test ./...
go test -run TestREADMECodeBlocks

Linting clean:

golangci-lint run ./...

Notes

  • The existing oauth.go already handles DPoP for token endpoint requests
  • The existing token_adding_interceptor.go already handles DPoP for gRPC/Connect
  • This PR adds the missing piece: DPoP for plain HTTP calls (KAS rewrap, etc.)
  • The RoundTripper pattern is idiomatic Go and composes with any http.Client

Jira: DSPX-3397
Test Scenario: xtest/scenarios/DSPX-3397.yaml

Summary by CodeRabbit

  • New Features
    • Added DPoP (RFC 9449) support for sender-constrained tokens and DPoP-bound requests, configurable via algorithm, PEM key, or JWK.
    • Added --dpop and --dpop-key flags to encrypt and decrypt commands.
    • Added exported SDK feature reporting for DPoP and Connect RPC.
  • Bug Fixes
    • Improved DPoP nonce challenge handling with a single safe retry and request-body replay.
    • Refined origin/URI normalization used for DPoP proof validation.
  • Documentation
    • Updated encrypt/decrypt manuals for new DPoP flags and interactions.
  • Tests
    • Added comprehensive coverage for DPoP transport, nonce retry, key handling, and validation HTTP client; extended xtest guidance.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds DPoP configuration, transport, and SDK/client wiring for token-bound requests, plus CLI flag plumbing and host-origin normalization in service auth.

Changes

DPoP Support

Layer / File(s) Summary
SDK config and key resolution
sdk/options.go, sdk/version.go, sdk/dpop_key.go, sdk/dpop_key_test.go
Adds DPoP option constructors and config fields, reports supported features, and implements key generation, PEM loading, algorithm validation, key resolution, and tests.
JWK-based token sources
sdk/idp_access_token_source.go, sdk/idp_cert_exchange.go, sdk/idp_oauth_access_token_source.go, sdk/idp_token_exchange_token_source.go
Adds constructors that build token sources from pre-resolved DPoP JWK keys.
DPoP HTTP transport
sdk/auth/dpop_transport.go, sdk/auth/dpop_transport_test.go, sdk/dpop_validation_client_test.go
Adds the DPoP transport, proof generation, nonce retry and body replay handling, client wrapping, and transport tests.
SDK DPoP wiring
sdk/sdk.go
Wraps SDK HTTP clients with the DPoP transport and routes token-source construction through JWK-based builders when needed.
Handler option plumbing
otdfctl/pkg/handlers/sdk.go, otdfctl/cmd/common/common.go, otdfctl/pkg/auth/auth.go
Exports handler option functions, threads extra SDK options into auth validation, and updates handler/auth client construction to accept the new option path.
CLI flags and docs
otdfctl/cmd/tdf/encrypt.go, otdfctl/cmd/tdf/decrypt.go, otdfctl/cmd/tdf/dpop.go, otdfctl/docs/man/..., AGENTS.md
Adds DPoP CLI flag parsing, passes the resulting SDK options into handler creation, updates the command manuals, and adds xtest branch-running instructions.
Host origin normalization
service/internal/auth/authn.go, service/internal/auth/authn_test.go
Adds normalized origin construction from Host headers and updates DPoP origin handling in the HTTP mux, with matching tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

  • opentdf/platform#3479: Also changes DPoP handling in service/internal/auth/authn.go, including validation-related origin and htu paths.

Suggested labels: comp:middleware:auth, docs

Suggested reviewers: elizabethhealy, jakedoublev, alkalescent

Poem

I packed up the proof in a carrot-lit jar,
Hopped through the nonce trail, near and far.
--dpop now sings with a tidy new tune,
And hosts line up under the moon.
🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding DPoP client support via an HTTP RoundTripper in the SDK.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch DSPX-3397-platform-go-sdk

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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

  • DPoP Transport Implementation: Introduced DPoPTransport, a new http.RoundTripper implementation that adds RFC 9449 DPoP proof tokens to HTTP requests, including support for server-issued nonce challenges and automatic retries.
  • SDK Integration: Updated the SDK to automatically wrap HTTP clients with DPoP support during construction, ensuring that resource requests are properly signed with DPoP proofs.
  • Feature Detection: Added a SupportedFeatures() function to sdk/version.go to allow integration harnesses to programmatically detect DPoP capability.
  • Testing: Added comprehensive unit tests for DPoP proof generation, nonce challenge flows, and URI normalization to ensure RFC 9449 compliance.
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 Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions Bot added comp:sdk A software development kit, including library, for client applications and inter-service communicati size/m labels Jun 8, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread sdk/auth/dpop_transport.go Outdated
Comment thread sdk/auth/dpop_transport.go Outdated
Comment thread sdk/auth/dpop_transport.go Outdated
Comment thread sdk/auth/dpop_transport.go Outdated
Comment thread sdk/auth/dpop_transport.go Outdated
Comment thread sdk/auth/dpop_transport.go Outdated
Comment thread sdk/auth/dpop_transport.go
Comment thread sdk/auth/dpop_transport.go Outdated
Comment thread sdk/auth/dpop_transport.go
Comment thread sdk/auth/dpop_transport.go
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 203.008006ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 103.232466ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 423.703616ms
Throughput 236.01 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 50.220444104s
Average Latency 497.588923ms
Throughput 99.56 requests/second

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 270.01327ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 144.386555ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 443.855047ms
Throughput 225.30 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 49.03497246s
Average Latency 487.426932ms
Throughput 101.97 requests/second

@dmihalcik-virtru dmihalcik-virtru force-pushed the DSPX-3397-platform-go-sdk branch from 441af7b to 61316ef Compare June 10, 2026 12:27
@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 183.335287ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 101.378765ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 435.240452ms
Throughput 229.76 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 50.575675367s
Average Latency 503.106935ms
Throughput 98.86 requests/second

@github-actions

Copy link
Copy Markdown
Contributor

@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 173.859448ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 89.451113ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 443.962682ms
Throughput 225.24 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 49.774071866s
Average Latency 493.399511ms
Throughput 100.45 requests/second

@dmihalcik-virtru dmihalcik-virtru force-pushed the DSPX-3397-platform-go-sdk branch from ebc3e40 to 37ed377 Compare June 11, 2026 17:47
@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 189.365241ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 92.807389ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 458.733975ms
Throughput 217.99 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 50.627558901s
Average Latency 502.758805ms
Throughput 98.76 requests/second

@dmihalcik-virtru dmihalcik-virtru force-pushed the DSPX-3397-platform-go-sdk branch from 37ed377 to b9dd8d8 Compare June 15, 2026 18:23
@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 174.336781ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 87.925096ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 451.175969ms
Throughput 221.64 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 49.075520706s
Average Latency 488.0126ms
Throughput 101.88 requests/second

@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 190.019238ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 100.783852ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 436.996106ms
Throughput 228.83 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 50.277491194s
Average Latency 498.407616ms
Throughput 99.45 requests/second

@dmihalcik-virtru dmihalcik-virtru force-pushed the DSPX-3397-platform-go-sdk branch from 5c4f57a to 4cec258 Compare June 18, 2026 13:08
@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 175.765414ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 93.303736ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 446.045553ms
Throughput 224.19 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 49.725004344s
Average Latency 495.004232ms
Throughput 100.55 requests/second

@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 193.716094ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 106.047078ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 418.095248ms
Throughput 239.18 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 50.244870901s
Average Latency 499.255302ms
Throughput 99.51 requests/second

@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 181.707776ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 99.144481ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 676.855143ms
Throughput 147.74 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 51.8725193s
Average Latency 515.398407ms
Throughput 96.39 requests/second

Replace the two overlapping DPoP key selectors (resolveDPoPKey and
pickDPoPKey) with a single pure resolveDPoPKey that follows one documented
priority: dpopJWK -> dpopKeyPEM -> dpopAlgorithm -> dpopKey(RSA) -> nil.

resolveDPoPKey no longer mutates c.dpopJWK. The previous read-with-hidden-write
cache only existed to share the ephemeral (dpopAlgorithm) key between the token
source and the DPoP transport, coupling them by call order. Instead the key is
now resolved once in buildIDPTokenSource and returned so SDK.New gives the
transport the same key the token source binds to.

buildIDPTokenSource now builds every flow via buildIDPTokenSourceFromJWK
(auto-generating the default RSA key and converting it with getDPoPJWK), and a
custom access token source combined with any DPoP key option now consistently
drives the transport.

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 194.474784ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 106.453749ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 427.770314ms
Throughput 233.77 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 46.235086292s
Average Latency 460.132529ms
Throughput 108.14 requests/second

normalizeURI stripped default ports with strings.HasSuffix on the raw
host, which is brittle around IPv6 literals (e.g. [fe80::443]). Use
u.Hostname()/u.Port() compared against the scheme default instead, via a
shared normalizedHostPort helper reused by getOrigin so the nonce-cache
key and htu claim normalize consistently. isTokenEndpointRequest now
compares normalized URIs so an explicit :443 matches a portless endpoint.

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
Parse the request Host with url.Hostname()/Port() instead of trimming a
":443"/":80" suffix, so IPv6 literals and hosts that merely end in the
default port are handled correctly. Lowercase the host to match the SDK's
htu normalization, since the DPoP htu match is an exact string comparison.

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
@dmihalcik-virtru dmihalcik-virtru requested a review from a team as a code owner July 1, 2026 10:50
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 205.209568ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 109.398047ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 431.227699ms
Throughput 231.90 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 46.49168518s
Average Latency 462.649469ms
Throughput 107.55 requests/second

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
sdk/sdk.go (1)

205-228: ⚠️ Potential issue | 🟠 Major

cfg.coreConn still bypasses the wrapped client and auth interceptors.

The client/interceptor pipeline built above never reaches the cfg.coreConn branch, so resource calls still go out through cfg.coreConn.Client without the DPoP transport, and custom token sources without DPoP also miss the Authorization interceptor. Build platformConn from the supplied endpoint/options but swap in the wrapped client, using cfg.coreConn.Client as the DPoP base when present.

Suggested fix
-	httpClient := cfg.httpClient
+	baseHTTPClient := cfg.httpClient
+	if cfg.coreConn != nil && cfg.coreConn.Client != nil {
+		baseHTTPClient = cfg.coreConn.Client
+	}
+	httpClient := baseHTTPClient
 	dpopHandledByTransport := false
 	if accessTokenSource != nil && dpopKey != nil {
-		httpClient = auth.NewDPoPHTTPClient(cfg.httpClient, dpopKey, accessTokenSource, cfg.tokenEndpoint)
+		httpClient = auth.NewDPoPHTTPClient(baseHTTPClient, dpopKey, accessTokenSource, cfg.tokenEndpoint)
 		dpopHandledByTransport = true
 	}
@@
 	if cfg.coreConn != nil {
-		platformConn = cfg.coreConn
+		platformConn = &ConnectRPCConnection{
+			Endpoint: cfg.coreConn.Endpoint,
+			Client:   httpClient,
+			Options:  append(cfg.coreConn.Options, connect.WithInterceptors(uci...)),
+		}
 	} else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/sdk.go` around lines 205 - 228, The `cfg.coreConn` branch in `sdk.go` is
bypassing the wrapped `httpClient` and the auth interceptor setup created in
this block. Update the `platformConn` construction so it always uses the same
client/auth pipeline: when `cfg.coreConn` is present, keep its endpoint/options
but replace its client with the wrapped client, and use `cfg.coreConn.Client` as
the DPoP base when applicable. Ensure the `ConnectRPCConnection` path and the
`cfg.coreConn` path both flow through the same `auth.NewDPoPHTTPClient` /
`NewTokenAddingInterceptorWithClient` behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sdk/auth/dpop_transport.go`:
- Around line 278-284: The URI normalization in normalizeURI currently uses
u.Path, which can decode escaped bytes and change the DPoP htu value. Update
normalizeURI in dpop_transport.go to preserve the original escaped path by using
u.EscapedPath() when building the normalized URI, while keeping the existing
scheme/host normalization and query/fragment stripping. Also add a test case
covering an escaped reserved path such as /a%2Fb to verify the normalized output
keeps the escape intact.

In `@sdk/sdk.go`:
- Around line 302-311: The credential setup logic in the SDK currently returns
the uncredentialed-client fast path before checking `certExchange` and
`tokenExchange`, which causes those exchange-based configurations to be ignored
and masks the dual-config error. Update the credential resolution flow in the
relevant SDK auth setup method so the `certExchange`/`tokenExchange` validation
and token-source creation run before the `clientCredentials == nil &&
oauthAccessTokenSource == nil` return. Keep the DPoP warning and uncredentialed
fallback only after exchange configurations have been handled.

---

Duplicate comments:
In `@sdk/sdk.go`:
- Around line 205-228: The `cfg.coreConn` branch in `sdk.go` is bypassing the
wrapped `httpClient` and the auth interceptor setup created in this block.
Update the `platformConn` construction so it always uses the same client/auth
pipeline: when `cfg.coreConn` is present, keep its endpoint/options but replace
its client with the wrapped client, and use `cfg.coreConn.Client` as the DPoP
base when applicable. Ensure the `ConnectRPCConnection` path and the
`cfg.coreConn` path both flow through the same `auth.NewDPoPHTTPClient` /
`NewTokenAddingInterceptorWithClient` behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 21b6a8bc-fc01-414c-aa6f-aa3b92fb9086

📥 Commits

Reviewing files that changed from the base of the PR and between e28fca0 and 59ed38d.

📒 Files selected for processing (8)
  • AGENTS.md
  • sdk/auth/dpop_transport.go
  • sdk/auth/dpop_transport_test.go
  • sdk/dpop_key.go
  • sdk/dpop_key_test.go
  • sdk/sdk.go
  • service/internal/auth/authn.go
  • service/internal/auth/authn_test.go

Comment thread sdk/auth/dpop_transport.go
Comment thread sdk/sdk.go Outdated
Wrap the caller-supplied oauth2.TokenSource in oauth2.ReuseTokenSource so a
valid token is reused across calls instead of re-fetched. AccessToken is on
the request hot path (once per gRPC/Connect call, plus once more to compute
the DPoP ath claim), so an uncached source risked an IdP round-trip on every
request. Wrapping an already-caching source is harmless.

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
- transport: fall through on successful nonce retry so a rotated DPoP-Nonce
  is cached instead of dropped by the early return
- transport: normalize htu with EscapedPath to preserve percent-encoded bytes
- dpop_key: reject public-only keys, ES*/RS* family mismatches, and EC
  curve/alg mismatches at resolution time via validateDPoPKey
- sdk: surface conflicting exchange config before the uncredentialed fast-path
- sdk/otdfctl: apply DPoP options during credential validation via
  NewDPoPValidationHTTPClient so pre-flight token requests carry a proof

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 201.638523ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 104.366441ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 467.064565ms
Throughput 214.10 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 47.252319975s
Average Latency 470.805021ms
Throughput 105.81 requests/second

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 212.474514ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 113.28738ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 428.859294ms
Throughput 233.18 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 46.126760524s
Average Latency 459.638437ms
Throughput 108.40 requests/second

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sdk/sdk.go (1)

346-354: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the intended ES256/P-256 default DPoP key.

This fallback still generates an RSA/RS256 key, while the PR objective says the SDK should default to an ephemeral P-256 ES256 key when none is supplied. That can bind tokens with a different algorithm than callers expect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/sdk.go` around lines 346 - 354, The DPoP fallback in sdk.go is still
generating an RSA key pair instead of the intended default ES256/P-256 key.
Update the dpopKey nil branch in the SDK initialization path to create an
ephemeral P-256 key for use with getDPoPJWK and store it on c.dpopKey, rather
than calling ocrypto.NewRSAKeyPair. Keep the existing error handling pattern,
but make sure the default algorithm and key type align with the DPoP
expectations in this constructor logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sdk/auth/dpop_transport.go`:
- Around line 95-100: The retry path in retryWithNonce from dpop_transport.go
still attempts to resend requests even when the body cannot be replayed, which
can consume a one-shot body and fail incorrectly. Update the nonce retry
handling around the retryWithNonce call to detect non-replayable requests (for
example when GetBody is nil after bufferRequestBody) and cache the nonce without
retrying. In that case, return the original 401 response instead of calling the
resend path for non-replayable bodies.

In `@sdk/dpop_key_test.go`:
- Around line 204-267: Add a regression test for the dpopKeyPEM + dpopAlgorithm
override path in TestValidateDPoPKey. The current cases only cover invalid
dpopJWK inputs, so add a mismatch scenario where selectDPoPKey resolves an RSA
PEM key but dpopAlgorithm forces ES256 (or equivalent) and assert it fails with
the expected algorithm/key-type error. Use the existing test helpers and config
fields in resolveDPoPKey/selectDPoPKey to keep the coverage on the override
path.

---

Outside diff comments:
In `@sdk/sdk.go`:
- Around line 346-354: The DPoP fallback in sdk.go is still generating an RSA
key pair instead of the intended default ES256/P-256 key. Update the dpopKey nil
branch in the SDK initialization path to create an ephemeral P-256 key for use
with getDPoPJWK and store it on c.dpopKey, rather than calling
ocrypto.NewRSAKeyPair. Keep the existing error handling pattern, but make sure
the default algorithm and key type align with the DPoP expectations in this
constructor logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 13ea97c6-ec83-4a89-8d11-e9872f97f52f

📥 Commits

Reviewing files that changed from the base of the PR and between 59ed38d and db7a3db.

📒 Files selected for processing (10)
  • otdfctl/cmd/common/common.go
  • otdfctl/pkg/auth/auth.go
  • sdk/auth/dpop_transport.go
  • sdk/auth/dpop_transport_test.go
  • sdk/dpop_key.go
  • sdk/dpop_key_test.go
  • sdk/dpop_validation_client_test.go
  • sdk/idp_oauth_access_token_source.go
  • sdk/idp_oauth_access_token_source_test.go
  • sdk/sdk.go

Comment thread sdk/auth/dpop_transport.go
Comment thread sdk/dpop_key_test.go
The condition logic is intentionally written to be easily understood by
readers: don't append the port if it matches the default for its scheme.
De Morgan transformation would make this less readable.

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 207.827174ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 112.211302ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 436.584813ms
Throughput 229.05 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 46.001519117s
Average Latency 458.876242ms
Throughput 108.69 requests/second

- dpop_transport: don't retry nonce challenge when body is non-replayable
  (streaming/unknown-length body consumed on first attempt); cache the
  nonce and return the original 401 instead of resending an empty body
- sdk: default to an ephemeral ES256/P-256 DPoP key instead of RSA when
  none is configured, matching documented behavior
- dpop_key_test: add regression case for RSA PEM overridden to ES256

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 210.819982ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 100.921714ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 447.69108ms
Throughput 223.37 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 38.433639763s
Average Latency 383.464569ms
Throughput 130.09 requests/second

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 154.833714ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 77.110413ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 350.104631ms
Throughput 285.63 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 30.562412492s
Average Latency 305.100962ms
Throughput 163.60 requests/second

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

⚠️ Govulncheck found vulnerabilities ⚠️

The following modules have known vulnerabilities:

  • examples
  • otdfctl
  • sdk
  • service
  • lib/fixtures
  • tests-bdd

See the workflow run for details.

@pflynn-virtru pflynn-virtru left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no security concerns found

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp:sdk A software development kit, including library, for client applications and inter-service communicati size/m

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants