Skip to content

feat(platform): add comprehensive DPoP (RFC 9449) support (DSPX-3397)#3582

Open
dmihalcik-virtru wants to merge 13 commits into
mainfrom
DSPX-3397-platform-service
Open

feat(platform): add comprehensive DPoP (RFC 9449) support (DSPX-3397)#3582
dmihalcik-virtru wants to merge 13 commits into
mainfrom
DSPX-3397-platform-service

Conversation

@dmihalcik-virtru

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

Copy link
Copy Markdown
Member

Implements comprehensive DPoP (Demonstrating Proof-of-Possession) support per RFC 9449 for the OpenTDF platform service.

Note: Providing DPoP support at the application server may not fit your deployment strategy! If you want a high-availability or multi-region support, consider implementing DPoP with an application gateway or proxy

Summary

This PR adds full RFC 9449 DPoP support to the platform authentication middleware, enabling:

  • ✅ Both Authorization: Bearer and Authorization: DPoP token schemes
  • ✅ Complete DPoP proof validation (typ, alg, jwk, signature, htm, htu, ath, jkt)
  • ✅ Server-issued DPoP-Nonce challenges with rotation (RFC 9449 §8)
  • ✅ gRPC support (htm=POST, full service paths as htu)
  • ✅ Feature detection via wellknown service (supports_dpop)
  • ✅ Comprehensive unit tests

Since we have an existing, partial implementation, some fallback protection is in place:

  • Continue to allow DPoP requiring JWTs in Authorization: Bearer [jwt] headers.
    • Emit WARN server message if a Bearer token has the cnf claim.
  • Continue to support htu claims with missing origin, e.g. /kas.AccessService/Rewrap will match
    • To require full htu, set server.auth.dpop.strict_htu: true

Implementation Details

DPoP Proof Validation (RFC 9449 §4.3 + §7.1)

The middleware validates all required DPoP proof claims:

  • typ: Must be dpop+jwt
  • alg: Only asymmetric algorithms (ES256/384/512, RS256/384/512, PS256/384/512)
  • jwk: Public key embedded in header
  • Signature: Verified against embedded JWK
  • htm: HTTP method must match request
  • htu: Normalized URI must match request
  • ath: SHA-256 hash of access token (base64url)
  • jkt: RFC 7638 JWK thumbprint in access token's cnf.jkt claim

DPoP-Nonce Support (RFC 9449 §8)

Server-issued nonces prevent replay attacks:

  • Configuration: server.auth.dpop.require_nonce (default: false), server.auth.dpop.nonce_expiration (default: 5m)
  • Challenge flow: Missing/invalid nonce → 401 with DPoP-Nonce header and WWW-Authenticate: DPoP error="use_dpop_nonce"
  • Validation window: Current + previous nonce (allows graceful rotation)
  • Rotation: Automatic based on expiration interval
  • HTTP & gRPC: Works in both MuxHandler (HTTP) and ConnectUnaryServerInterceptor (gRPC)

gRPC Support

DPoP works seamlessly with gRPC/Connect:

  • htm: Always POST for gRPC calls
  • htu: Full procedure path (e.g., /kas.AccessService/Rewrap)
  • DPoP-Nonce: Propagated via response headers/trailers

Feature Detection

Registers supports_dpop: true in the wellknown service for integration with xtest feature gates (pfs.skip_if_unsupported("dpop")).

Testing

Unit Tests (dpop_nonce_test.go)

Comprehensive test coverage:

  • Nonce generation, rotation, validation window
  • Proof validation (signature, htm, htu, ath, jkt)
  • Algorithm restrictions (asymmetric only)
  • Error types and detection
  • Token expiration handling

Integration Tests

Existing DPoP tests in test/integration/oauth/oauth_test.go continue to pass and validate end-to-end flows with real Keycloak.

Related

  • Parent Jira: DSPX-3397
  • Test scenario: xtest/scenarios/DSPX-3397.yaml
  • SDK implementations: platform-go-sdk, java-sdk, web-sdk (sibling PRs in this feature)

All PRs:

Summary by CodeRabbit

Release Notes

  • New Features

    • Added server-side DPoP nonce challenges with rotating nonce support, including nonce-specific authentication failures and nonce headers on successful responses and rejections.
    • Improved DPoP proof validation with configurable strict HTU matching and better URI handling.
  • Configuration

    • Added DPoP nonce settings (require nonce, expiration, strict HTU) plus a startup toggle to enable nonce challenges.
  • Chores

    • Upgraded Keycloak to 26.2, expanded enabled Keycloak features, and added an additional DPoP-capable client.
    • Expanded DPoP test coverage and added extra debug logging.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@dmihalcik-virtru, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 42 minutes and 20 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2c0b9151-143d-45dd-9ee9-59a5c3f90802

📥 Commits

Reviewing files that changed from the base of the PR and between 70649bc and 3939167.

📒 Files selected for processing (2)
  • service/kas/access/accessPdp.go
  • service/kas/access/rewrap.go
📝 Walkthrough

Walkthrough

Keycloak is upgraded to 26.2 with admin-fine-grained-authz enabled and a new opentdf-dpop client configured with DPoP-bound token support. A DPoPConfig struct with nonce enforcement and HTU matching options is added to AuthNConfig. The authenticator gains a concurrent rotating nonce manager, a matchHTU helper for strict/loose HTU validation, and two exported nonce error types (DPoPNonceError for retryable failures, DPoPNonceMalformedError for hard rejections), with nonce challenge propagation wired through HTTP mux and Connect middleware paths including receiver URI construction. Tests cover nonce manager rotation and eviction, error type detection, algorithm restrictions, HTTP method propagation, HTU matching modes, and end-to-end checkToken scenarios with missing, malformed, wrong, and valid nonces. A CI composite action gains a dpop-challenge-enabled input and updated bootstrap references. SDK-side logging enhancements capture DPoP construction context.

Changes

DPoP nonce enforcement and Keycloak 26.2 upgrade

Layer / File(s) Summary
Keycloak 26.2 and opentdf-dpop client
docker-compose.yaml, service/cmd/keycloak_data.yaml
Upgrades Keycloak image from 25.0 to 26.2, adds admin-fine-grained-authz:v1 to KC_FEATURES, and introduces the opentdf-dpop Keycloak client with service accounts, DPoP-bound token attributes (dpop.bound.access.tokens: "true"), and opentdf-admin role.
DPoPConfig struct and validation
service/internal/auth/config.go
Adds DPoPConfig with require_nonce, nonce_expiration, and strict_htu fields; implements DPoPConfig.Validate() to enforce positive expiration when nonce enforcement is enabled; and wires validation into AuthNConfig.Validate().
Nonce manager, error types, matchHTU, and NewAuthenticator wiring
service/internal/auth/authn.go
Adds imports for cryptographic randomness and concurrent synchronization. Implements dpopNonceManager with concurrent rotating nonce state (current, previous, evicted window). Extends Authentication with strictDPoPHTU and dpopNonceManager fields. Exports DPoPNonceError (retryable) and DPoPNonceMalformedError (hard rejection) types. Implements matchHTU to validate DPoP htu claims with strict and loose modes. NewAuthenticator registers DPoP well-known metadata including supported algorithms and nonce requirement.
Nonce challenge propagation in middleware and token validation
service/internal/auth/authn.go
MuxHandler returns DPoP-Nonce challenge and WWW-Authenticate: use_dpop_nonce on DPoPNonceError, and injects fresh nonce on success. ConnectAuthNInterceptor constructs http/https receiver URIs from Host plus procedure, propagates nonce challenges via Connect error metadata, and injects fresh nonce header on success. checkToken explicitly parses Authorization scheme, warns on Bearer scheme with cnf-bound tokens, and emits nonce-specific debug logs. validateDPoP uses matchHTU for htu validation and adds conditional nonce claim validation with typed error returns. ipcReauthCheck constructs http/https receiver URIs from IPC Host plus RPC path.
HTTP method propagation and matchHTU tests
service/internal/auth/authn_test.go
Extends authnTestRequest with httpMethod field and HTTPMethod() method. Adds test case for incorrect DPoP htm claim with GET. Test_CheckToken_AcceptsDPoP_GET verifies checkToken accepts DPoP proof with htm: GET. Test_ConnectAuthNInterceptor_PropagatesHTTPMethod verifies interceptor forwards actual HTTP method into receiverInfo. TestMatchHTU table-tests matchHTU in loose and strict modes.
Nonce manager and error type unit and integration tests
service/internal/auth/dpop_nonce_test.go
TestDPoPNonceManager verifies nonce generation (32-character length), rotation after expiration, eviction of older nonces, and required/disabled nonce behavior. TestDPoPNonceError and TestDPoPAlgorithmRestrictions validate error types and allowed JWS signature algorithms. AuthSuite integration tests use helpers (newAuthWithNonce, makeDPoPBoundAccessToken, makeDPoPProof, newDPoPKeyAndAccessToken) to assert checkToken returns correct typed errors for missing, malformed, wrong, and valid nonces.
CI action dpop-challenge-enabled input and bootstrap tag update
test/start-up-with-containers/action.yaml
Adds dpop-challenge-enabled boolean input (default false) with runtime validation. Exports DPOP_CHALLENGE_ENABLED environment variable. Updates bootstrap script downloads to feat-kc26-dpop tag. Conditionally sets .server.auth.dpop.require_nonce = true in opentdf.yaml when enabled.
SDK-side DPoP logging
sdk/auth/oauth/oauth.go, sdk/auth/token_adding_interceptor.go
Adds slog.Debug logging to DPoP token construction paths on the client side, including HTTP method, endpoint, nonce presence in getDPoPAssertion, and gRPC/Connect method details plus htm/htu values in token interceptor paths.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MuxHandler
  participant checkToken
  participant validateDPoP
  participant dpopNonceManager

  Client->>MuxHandler: HTTP request (Authorization: DPoP <token>, DPoP: <proof>)
  MuxHandler->>checkToken: extract scheme + token
  checkToken->>validateDPoP: verify DPoP proof, htu via matchHTU
  validateDPoP->>dpopNonceManager: ValidateNonce(nonce claim)

  alt nonce missing or expired
    dpopNonceManager-->>validateDPoP: DPoPNonceError
    validateDPoP-->>checkToken: DPoPNonceError
    checkToken-->>MuxHandler: DPoPNonceError
    MuxHandler-->>Client: 401 + DPoP-Nonce: <fresh> + WWW-Authenticate: use_dpop_nonce
  else nonce malformed (non-string)
    dpopNonceManager-->>validateDPoP: DPoPNonceMalformedError
    validateDPoP-->>MuxHandler: DPoPNonceMalformedError
    MuxHandler-->>Client: 401 unauthenticated (no retry hint)
  else valid nonce
    dpopNonceManager-->>validateDPoP: ok
    validateDPoP-->>MuxHandler: authenticated principal
    MuxHandler-->>Client: 200 + DPoP-Nonce: <fresh>
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

size/xl

Suggested reviewers

  • jakedoublev
  • pflynn-virtru

Poem

🐇 Hop hop, the nonce rotates with care,
A fresh DPoP token floats through the air.
Keycloak leaps up to version twenty-six,
Strict HTU and loose mode—no more tricks!
The rabbit stamps proofs with a joyful bound,
For nonce-bound tokens are safe and sound. 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the main change: comprehensive DPoP (RFC 9449) support implementation across the platform.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch DSPX-3397-platform-service

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 and usage tips.

@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 introduces full RFC 9449 DPoP support to the OpenTDF platform service. By integrating DPoP proof validation and server-issued nonce management, the changes significantly enhance the security of token-based authentication for both HTTP and gRPC interfaces. The implementation includes robust error handling for nonce challenges and ensures compatibility with existing authentication flows.

Highlights

  • DPoP Implementation: Added comprehensive support for RFC 9449 (Demonstrating Proof-of-Possession) in the authentication middleware, including support for both Bearer and DPoP token schemes.
  • Nonce Management: Implemented a thread-safe DPoP-Nonce manager to handle server-issued challenges, rotation, and validation windows to prevent replay attacks.
  • gRPC & Feature Support: Extended DPoP support to gRPC/Connect services and added a 'supports_dpop' flag to the wellknown service for feature detection.
  • Testing: Added a new comprehensive test suite in dpop_nonce_test.go covering nonce generation, rotation, proof validation, and error handling.
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.


Tokens held with proof of key, RFC standards, plain to see. Nonces rotate, threats subside, Security with nowhere to hide.

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.

@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 server-issued DPoP nonce management per RFC 9449 §8, including configuration options, a nonce manager with rotation and validation, and interceptor integrations for both HTTP and Connect handlers. Feedback on these changes highlights two critical issues: a race condition in getCurrentNonce() that can cause concurrent double-rotation of nonces, and incorrect response header propagation in the Connect interceptor where metadata.AppendToOutgoingContext is used instead of setting headers on the response directly.

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 service/internal/auth/authn.go
Comment thread service/internal/auth/authn.go Outdated
@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 194.021909ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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 433.667984ms
Throughput 230.59 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 44.119547432s
Average Latency 439.374601ms
Throughput 113.33 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 183.274907ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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 445.131147ms
Throughput 224.65 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 50.553668801s
Average Latency 503.209897ms
Throughput 98.90 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 189.781332ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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 411.447772ms
Throughput 243.04 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 50.925251017s
Average Latency 507.600038ms
Throughput 98.18 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 186.574335ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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 452.884242ms
Throughput 220.81 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 54.049265382s
Average Latency 538.485137ms
Throughput 92.51 requests/second

Comment thread service/internal/auth/authn.go Outdated
Comment thread service/internal/auth/authn.go Outdated
Comment thread service/internal/auth/authn.go
@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 156.592218ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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 363.844486ms
Throughput 274.84 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 40.901048761s
Average Latency 406.803182ms
Throughput 122.25 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 177.80397ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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.215437ms
Throughput 229.77 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 50.814660928s
Average Latency 505.254278ms
Throughput 98.40 requests/second

@dmihalcik-virtru dmihalcik-virtru force-pushed the DSPX-3397-platform-service branch from f8d30ac to 77a7d4d 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 185.821117ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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 426.508284ms
Throughput 234.46 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 53.965016947s
Average Latency 537.637643ms
Throughput 92.65 requests/second

@dmihalcik-virtru dmihalcik-virtru force-pushed the DSPX-3397-platform-service branch from 77a7d4d to d0155a6 Compare June 12, 2026 19:04
@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 178.741126ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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 448.799843ms
Throughput 222.82 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 48.897792112s
Average Latency 486.825249ms
Throughput 102.25 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 184.961868ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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 422.563618ms
Throughput 236.65 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 51.459641967s
Average Latency 512.719441ms
Throughput 97.16 requests/second

@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Error unauthenticated: unauthenticated
Total Time 12.095811ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Error unauthenticated: unauthenticated
Total Time 12.979292ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 0
Failed Decrypts 100
Total Time 4.796253ms
Throughput 0.00 requests/second

Error Summary

Error Message Occurrences
failed to get allowlist from registry: kasregistry.ListKeyAccessServers failed: unauthenticated: unauthenticated 1 occurrences

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 0
Failed Requests 5000
Concurrent Requests 50
Total Time 3.41727433s
Throughput 0.00 requests/second

Error Summary:

Error Message Occurrences
LoadTDF error: allowListFromKASRegistry failed: kasregistry.ListKeyAccessServers failed: unauthenticated: unauthenticated 5000 occurrences

@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 141.492353ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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.999778ms
Throughput 233.64 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 47.613264897s
Average Latency 473.910981ms
Throughput 105.01 requests/second

@dmihalcik-virtru dmihalcik-virtru marked this pull request as ready for review June 16, 2026 15:20
@dmihalcik-virtru dmihalcik-virtru requested a review from a team as a code owner June 16, 2026 15:20

@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

🤖 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 `@service/internal/auth/authn.go`:
- Around line 518-523: The code pattern where `errors.As` extracts a
`*connect.Error` into `connectErr`, modifies it via
`connectErr.Meta().Set("DPoP-Nonce"...)`, and then returns the original `err` is
subtle and could be clearer. Add a clarifying comment above the `if
errors.As(err, &connectErr)` block explaining that `errors.As` extracts a
pointer to the underlying `*connect.Error` within the error chain, so modifying
`connectErr.Meta()` mutates the same underlying object that is part of the
returned `err`, making the return of `err` correct despite the modifications
being made on `connectErr`.

In `@test/start-up-with-containers/action.yaml`:
- Around line 144-146: The three curl commands downloading init-temp-keys.sh,
docker-compose.yaml, and watch.sh are using a hardcoded feature tag
`feat-kc26-dpop` instead of using the `platform-ref` input parameter that is
already defined and validated in the action. Replace the hardcoded
`feat-kc26-dpop` tag in all three curl URLs with `${{ inputs.platform-ref }}` to
align with the action's existing parameter usage and prevent breakage when the
feature tag is deleted.
🪄 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: ca44809a-aa60-40fe-929d-0383d289be78

📥 Commits

Reviewing files that changed from the base of the PR and between 6f832d6 and 24d7101.

📒 Files selected for processing (7)
  • docker-compose.yaml
  • service/cmd/keycloak_data.yaml
  • service/internal/auth/authn.go
  • service/internal/auth/authn_test.go
  • service/internal/auth/config.go
  • service/internal/auth/dpop_nonce_test.go
  • test/start-up-with-containers/action.yaml

Comment thread service/internal/auth/authn.go
Comment on lines +144 to +146
curl -sSfL https://raw.githubusercontent.com/opentdf/platform/refs/tags/feat-kc26-dpop/.github/scripts/init-temp-keys.sh > otdf-test-platform/.github/scripts/init-temp-keys.sh
curl -sSfL https://raw.githubusercontent.com/opentdf/platform/refs/tags/feat-kc26-dpop/docker-compose.yaml > otdf-test-platform/docker-compose.yaml
curl -sSfL https://raw.githubusercontent.com/opentdf/platform/refs/tags/feat-kc26-dpop/.github/scripts/watch.sh > otdf-test-platform/.github/scripts/watch.sh

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the feat-kc26-dpop tag exists and what it references
gh api repos/opentdf/platform/git/refs/tags/feat-kc26-dpop 2>/dev/null || echo "Tag feat-kc26-dpop not found or is a branch reference"

# Check if it's a branch instead
gh api repos/opentdf/platform/branches/feat-kc26-dpop 2>/dev/null && echo "feat-kc26-dpop is a branch"

Repository: opentdf/platform

Length of output: 547


🏁 Script executed:

# Check the full context of action.yaml around lines 144-146
sed -n '140,150p' test/start-up-with-containers/action.yaml

# Check if there's a platform-ref input parameter that could be used
grep -n "platform-ref\|platform_ref" test/start-up-with-containers/action.yaml | head -20

# Check if these scripts exist on main branch
curl -sSfL https://raw.githubusercontent.com/opentdf/platform/main/.github/scripts/init-temp-keys.sh > /dev/null 2>&1 && echo "init-temp-keys.sh exists on main" || echo "init-temp-keys.sh NOT found on main"
curl -sSfL https://raw.githubusercontent.com/opentdf/platform/main/.github/scripts/watch.sh > /dev/null 2>&1 && echo "watch.sh exists on main" || echo "watch.sh NOT found on main"
curl -sSfL https://raw.githubusercontent.com/opentdf/platform/main/docker-compose.yaml > /dev/null 2>&1 && echo "docker-compose.yaml exists on main" || echo "docker-compose.yaml NOT found on main"

Repository: opentdf/platform

Length of output: 1292


Use platform-ref input instead of hardcoded feature tag.

The downloads on lines 144-146 reference the hardcoded feat-kc26-dpop tag, but these files (init-temp-keys.sh, watch.sh, docker-compose.yaml) already exist on the main branch. Since the action already has a platform-ref input parameter (line 6) that is validated and used elsewhere (line 139), update these downloads to use ${{ inputs.platform-ref }} instead. This will align with the action's existing parameter usage and prevent breakage when the feature tag is eventually deleted.

🤖 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 `@test/start-up-with-containers/action.yaml` around lines 144 - 146, The three
curl commands downloading init-temp-keys.sh, docker-compose.yaml, and watch.sh
are using a hardcoded feature tag `feat-kc26-dpop` instead of using the
`platform-ref` input parameter that is already defined and validated in the
action. Replace the hardcoded `feat-kc26-dpop` tag in all three curl URLs with
`${{ inputs.platform-ref }}` to align with the action's existing parameter usage
and prevent breakage when the feature tag is deleted.

dmihalcik-virtru and others added 11 commits June 16, 2026 20:17
Implement RFC 9449 DPoP support for the OpenTDF platform service:

- Accept both `Authorization: Bearer` and `Authorization: DPoP` schemes
- Validate DPoP proofs per RFC 9449 §4.3 + §7.1:
  - typ=dpop+jwt header validation
  - Allowed algorithms: ES256, RS256, PS256 (and 384/512 variants)
  - JWK extraction and signature verification
  - htm/htu/ath claim validation
  - RFC 7638 JWK thumbprint matching cnf.jkt

- Server-issued DPoP-Nonce challenges per RFC 9449 §8:
  - Configurable via `server.auth.dpop.require_nonce` (default: false)
  - 401 response with `DPoP-Nonce` header and `WWW-Authenticate: DPoP error="use_dpop_nonce"`
  - Nonce rotation window (current + previous) with configurable expiration
  - Nonce validation in both HTTP and gRPC paths

- gRPC support: htm=POST, htu=full service path, DPoP-Nonce via response headers/trailers

- Feature detection: register `supports_dpop` in wellknown service for xtest integration

- Comprehensive unit tests for nonce management, proof validation, error handling

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…once action input, and KC26 bump (DSPX-3397)

- Register dpop_supported_alg_values and dpop_nonce_required in the
  /.well-known/opentdf-configuration endpoint so SDK clients can
  discover server DPoP capabilities (RFC 9449 §5.1)
- Add opentdf-dpop Keycloak client with dpop.bound.access.tokens=true
  attribute for DPoP-bound token testing alongside existing opentdf client
- Add dpop-challenge-enabled input to start-up-with-containers action
  that patches server.auth.dpop.require_nonce: true when enabled
- Update action curl downloads from pqc-enabled to feat-kc26-dpop tag
- Bump Keycloak from 25.0 to 26.2 in docker-compose.yaml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
Two fixups for the DSPX-3397 platform-service work:

- authn.go: structpb.NewStruct rejects []string when serializing the
  well-known configuration. Convert dpop_supported_alg_values to []any
  before registration. Without this, /.well-known/opentdf-configuration
  returns 500 with "proto: invalid type: []string".

- docker-compose.yaml: KC26 dropped admin-fine-grained-authz from the
  default preview profile. The platform's `service provision keycloak`
  calls setManagementPermissionsEnabled which requires this feature.
  Enable it explicitly via KC_FEATURES so provisioning succeeds against
  the bumped Keycloak image.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…gation

- getCurrentNonce: use double-checked locking to prevent concurrent
  double-rotation; inlines rotation under write lock instead of calling
  rotate() (which acquires its own lock and would deadlock)
- ConnectUnaryServerInterceptor: replace metadata.AppendToOutgoingContext
  with a next-wrapper that sets DPoP-Nonce on res.Header() so the header
  is actually returned to the client

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
- Rename well-known key to dpop_signing_alg_values_supported (RFC 9449 §5.1)
- Add DPoPNonceMalformedError type for non-string nonce claims; malformed
  proofs fall through to hard rejection rather than issuing a nonce challenge
- Downgrade DPoPNonceError log entries from Warn to Debug since nonce
  challenges are normal protocol handshakes, not failures
- Set DPoP-Nonce on Connect error responses so clients retain nonce after
  downstream handler errors
- Add DPoPConfig.Validate() to reject zero NonceExpiration when RequireNonce
  is true; call it from validateAuthNConfig
- Remove tautological tests that asserted only on local variables; replace
  with real checkToken coverage for missing/valid/wrong/malformed nonce cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
…empty nonce validation

Replace sync.RWMutex with atomic.Pointer[nonceState] for lock-free reads on
the hot path (getCurrentNonce, validateNonce), keeping sync.Mutex only to
serialize writes. Also guards validateNonce against empty-string nonce matching
the initial zero-value previousNonce.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
Per RFC 9449 §7.1, DPoP-bound access tokens (cnf.jkt claim present)
MUST be presented under the "DPoP" Authorization scheme. The platform
currently accepts either scheme as long as a valid DPoP proof is
attached. This change emits a WARN log when a cnf-bound token arrives
under "Bearer" scheme, surfacing non-compliant SDKs without breaking
existing clients. A follow-up will promote this to a hard reject once
all SDKs are compliant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ptor

The ConnectRPC interceptor was using req.Spec().Procedure (path only) as
the expected htu value, but RFC 9449 requires htu to be the full request
URI (scheme + host + path). Clients correctly send the full URL, causing
htu validation to always fail for gRPC/ConnectRPC endpoints.

Fix both the ConnectRPC unary interceptor and ipcReauthCheck to construct
the full URL from the Host header, accepting both http and https schemes
since TLS state is not available in the interceptor context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In loose mode (default, strict_htu: false) a path-only htu claim is
accepted when its path matches the path of any acceptable URI, easing
SDK skew during rollout. If the origin is present it must still match
exactly (scheme-flexible via the http+https pair already in dpopInfo.u).

In strict mode (strict_htu: true) the origin must be present and match;
path-only htu claims are rejected outright.

Config path: server.auth.dpop.strict_htu (bool, default false)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…interceptor

ConnectRPC supports idempotent unary RPCs over HTTP GET (proto
option idempotency_level = NO_SIDE_EFFECTS). The Java connect-go
client (Buf's 'Hasan'/Get-requests feature) uses this whenever the
server advertises an idempotent method. DPoP requires the proof
JWT's htm claim to equal the actual HTTP method, but the Connect
interceptor hardcoded POST in receiverInfo.m, so any GET request
with htm:'GET' was rejected as 'incorrect htm claim'.

Use connect.AnyRequest.HTTPMethod() (which is populated server-side
from the underlying *http.Request) to bring the Connect path to
parity with the MuxHandler path, which already reads r.Method.

IPCUnaryServerInterceptor is unchanged: IPC traffic goes through
memhttp which always uses POST.

Tests:
- New positive checkToken case for htm:GET.
- New interceptor test that captures receiverInfo via _testCheckTokenFunc
  and asserts the method is propagated (parameterized over GET and POST).
- New invalid-DPoP table row exercising the wrong-method failure
  direction (GET-DPoP against POST-only receiver).

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
@dmihalcik-virtru dmihalcik-virtru force-pushed the DSPX-3397-platform-service branch from 24d7101 to 70cb173 Compare June 17, 2026 00:37
@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 184.372141ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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 414.455601ms
Throughput 241.28 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 49.779208052s
Average Latency 496.136581ms
Throughput 100.44 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

🤖 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 `@service/internal/auth/authn_test.go`:
- Around line 786-804: The test case in the makeDPoPToken call and the
receiverInfo structure both use path-only htu values (such as
"/kas.AccessService/PublicKey"), which makes the test dependent on loose HTU
matching mode. To decouple the GET-method coverage from HTU validation behavior,
update the htu field in the makeDPoPToken call to use a full URL with scheme and
host (for example "http://localhost/kas.AccessService/PublicKey" or similar)
instead of just the path, ensuring the test can verify the htm=GET behavior
regardless of whether strict or loose HTU validation is enabled.

In `@service/internal/auth/dpop_nonce_test.go`:
- Around line 143-146: The DPoPConfig in the newAuthWithNonce() fixture does not
explicitly set the StrictHTU field, which leaves the HTU validation mode
implicit. Since the nonce tests use path-only htu values (as seen in test cases
at various lines), if the default HTU behavior changes, these tests could fail
for the wrong reason rather than exercising nonce validation. Add the StrictHTU
field to the DPoPConfig structure in newAuthWithNonce() and pin it to an
explicit value (likely false to match the path-only htu usage in the tests) to
ensure the tests remain deterministic and focused on nonce functionality.
🪄 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: 6b2d448e-a4c0-4260-989c-45e3b7d5ecec

📥 Commits

Reviewing files that changed from the base of the PR and between 24d7101 and 70cb173.

📒 Files selected for processing (7)
  • docker-compose.yaml
  • service/cmd/keycloak_data.yaml
  • service/internal/auth/authn.go
  • service/internal/auth/authn_test.go
  • service/internal/auth/config.go
  • service/internal/auth/dpop_nonce_test.go
  • test/start-up-with-containers/action.yaml

Comment on lines +786 to +804
dpopToken := makeDPoPToken(s.T(), dpopTestCase{
key: dpopPublic,
actualSigningKey: dpopKey,
accessToken: signedTok,
alg: jwa.RS256,
typ: "dpop+jwt",
htm: http.MethodGet,
htu: "/kas.AccessService/PublicKey",
iat: time.Now(),
})

_, _, err = s.auth.checkToken(
context.Background(),
[]string{"DPoP " + string(signedTok)},
receiverInfo{
u: []string{"/kas.AccessService/PublicKey"},
m: []string{http.MethodGet},
},
[]string{dpopToken},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Decouple GET-method coverage from loose HTU matching.

Line 793 and Line 801 use path-only htu, so this test also depends on loose HTU mode. If strict HTU is enabled later, it will fail before reaching the htm=GET behavior this test intends to verify.

Proposed adjustment
 	dpopToken := makeDPoPToken(s.T(), dpopTestCase{
 		key:              dpopPublic,
 		actualSigningKey: dpopKey,
 		accessToken:      signedTok,
 		alg:              jwa.RS256,
 		typ:              "dpop+jwt",
 		htm:              http.MethodGet,
-		htu:              "/kas.AccessService/PublicKey",
+		htu:              "https://localhost:8080/kas.AccessService/PublicKey",
 		iat:              time.Now(),
 	})
@@
 		[]string{"DPoP " + string(signedTok)},
 		receiverInfo{
-			u: []string{"/kas.AccessService/PublicKey"},
+			u: []string{
+				"http://localhost:8080/kas.AccessService/PublicKey",
+				"https://localhost:8080/kas.AccessService/PublicKey",
+			},
 			m: []string{http.MethodGet},
 		},
 		[]string{dpopToken},
 	)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
dpopToken := makeDPoPToken(s.T(), dpopTestCase{
key: dpopPublic,
actualSigningKey: dpopKey,
accessToken: signedTok,
alg: jwa.RS256,
typ: "dpop+jwt",
htm: http.MethodGet,
htu: "/kas.AccessService/PublicKey",
iat: time.Now(),
})
_, _, err = s.auth.checkToken(
context.Background(),
[]string{"DPoP " + string(signedTok)},
receiverInfo{
u: []string{"/kas.AccessService/PublicKey"},
m: []string{http.MethodGet},
},
[]string{dpopToken},
dpopToken := makeDPoPToken(s.T(), dpopTestCase{
key: dpopPublic,
actualSigningKey: dpopKey,
accessToken: signedTok,
alg: jwa.RS256,
typ: "dpop+jwt",
htm: http.MethodGet,
htu: "https://localhost:8080/kas.AccessService/PublicKey",
iat: time.Now(),
})
_, _, err = s.auth.checkToken(
context.Background(),
[]string{"DPoP " + string(signedTok)},
receiverInfo{
u: []string{
"http://localhost:8080/kas.AccessService/PublicKey",
"https://localhost:8080/kas.AccessService/PublicKey",
},
m: []string{http.MethodGet},
},
[]string{dpopToken},
🤖 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 `@service/internal/auth/authn_test.go` around lines 786 - 804, The test case in
the makeDPoPToken call and the receiverInfo structure both use path-only htu
values (such as "/kas.AccessService/PublicKey"), which makes the test dependent
on loose HTU matching mode. To decouple the GET-method coverage from HTU
validation behavior, update the htu field in the makeDPoPToken call to use a
full URL with scheme and host (for example
"http://localhost/kas.AccessService/PublicKey" or similar) instead of just the
path, ensuring the test can verify the htm=GET behavior regardless of whether
strict or loose HTU validation is enabled.

Comment on lines +143 to +146
DPoP: DPoPConfig{
RequireNonce: true,
NonceExpiration: 5 * time.Minute,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Pin StrictHTU in the nonce fixture to keep nonce tests deterministic.

newAuthWithNonce() leaves HTU mode implicit while nonce tests use path-only htu (for example, Line 229, Line 251, Line 271, Line 295). If strict HTU behavior changes, these tests can fail before nonce validation is exercised.

Proposed adjustment
 				DPoP: DPoPConfig{
 					RequireNonce:    true,
 					NonceExpiration: 5 * time.Minute,
+					StrictHTU:       false,
 				},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DPoP: DPoPConfig{
RequireNonce: true,
NonceExpiration: 5 * time.Minute,
},
DPoP: DPoPConfig{
RequireNonce: true,
NonceExpiration: 5 * time.Minute,
StrictHTU: false,
},
🤖 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 `@service/internal/auth/dpop_nonce_test.go` around lines 143 - 146, The
DPoPConfig in the newAuthWithNonce() fixture does not explicitly set the
StrictHTU field, which leaves the HTU validation mode implicit. Since the nonce
tests use path-only htu values (as seen in test cases at various lines), if the
default HTU behavior changes, these tests could fail for the wrong reason rather
than exercising nonce validation. Add the StrictHTU field to the DPoPConfig
structure in newAuthWithNonce() and pin it to an explicit value (likely false to
match the path-only htu usage in the tests) to ensure the tests remain
deterministic and focused on nonce functionality.

Log the htm claim at every site where DpopInfo.m is set on the server
and where the htm claim is built on the client, plus the htm comparison
in validateDPoP. Helps diagnose method mismatches between client and
server (e.g. client hardcoding POST while server reads the transport
method).

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
@dmihalcik-virtru dmihalcik-virtru requested review from a team as code owners June 17, 2026 12:30
@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 163.448517ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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 429.356287ms
Throughput 232.91 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 51.394884114s
Average Latency 511.421807ms
Throughput 97.29 requests/second

Map errors from canAccess into categorized 403/500 responses with a
sanitized cause-class tag (forbidden: pdp-denied,
internal: auth-service-unavailable, internal: context-cancelled) so
clients and tests can distinguish routine denials from infrastructure
failures without parsing logs. Previously every category from canAccess
collapsed to code=Internal with the opaque message
"could not perform access".

Split logging at the two GetDecision call sites and the rewrap call
site into Info (terse, no sensitive payload — category, connect code,
batch size) plus Debug (full error, policies, resource attribute FQNs,
fulfillable obligation FQNs). The Info lines previously dropped err
entirely at ErrorContext, so debug logging in CI couldn't surface
what the authorization service actually said.

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
@dmihalcik-virtru dmihalcik-virtru requested a review from a team as a code owner June 17, 2026 19:01
@github-actions

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.

@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.24482ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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 408.39241ms
Throughput 244.86 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 47.97123142s
Average Latency 478.306459ms
Throughput 104.23 requests/second

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants