Skip to content

feat(core): Adds comprehensive DPoP (RFC 9449) support#3582

Merged
dmihalcik-virtru merged 20 commits into
mainfrom
DSPX-3397-platform-service
Jun 22, 2026
Merged

feat(core): Adds comprehensive DPoP (RFC 9449) support#3582
dmihalcik-virtru merged 20 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
  • ✅ Extensive debug logging, which should be reduced after clients are up to spec

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.

End-to-End Tests

Validated end-to-end with the xtest DPoP scenario: opentdf/tests#529.

Note: The default docker-compose.yaml stays on Keycloak 25 so downstream consumers that aren't DPoP-ready are unaffected. The start-up-with-containers action overlays Keycloak 26.2 (+ admin-fine-grained-authz:v1) onto the compose file only when invoked with dpop-challenge-enabled: true. Wiring a CI job to pass that flag is left for a follow-up step.

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

  • New Features

    • Added Demonstration of Proof-of-Possession (DPoP) nonce validation support for enhanced token security.
    • Enabled configurable DPoP settings including nonce enforcement, expiration policies, and validation modes.
  • Improvements

    • Enhanced error classification and diagnostic logging for access control decisions and token operations.

@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 server-side DPoP nonce enforcement with a rotating concurrent nonce manager, strict/loose HTU matching, nonce challenge headers in HTTP mux and Connect middleware, exported nonce error types, DPoPConfig validation, a new opentdf-dpop Keycloak client, CI action support for DPoP nonce testing, SDK-side DPoP debug logging, and KAS access error classification.

Changes

DPoP Nonce Enforcement and Keycloak 26.2 Upgrade

Layer / File(s) Summary
Keycloak opentdf-dpop client and CI wiring
service/cmd/keycloak_data.yaml, test/start-up-with-containers/action.yaml
Adds opentdf-dpop Keycloak client with DPoP-bound token attribute and service account roles; adds dpop-challenge-enabled CI action input that conditionally enables require_nonce in opentdf.yaml and switches Keycloak to image keycloak/keycloak:26.2 with admin-fine-grained-authz:v1 feature.
DPoPConfig struct and validation
service/internal/auth/config.go
Adds DPoPConfig with require_nonce, nonce_expiration, and strict_htu fields to AuthNConfig; implements Validate() to reject non-positive nonce_expiration when require_nonce is enabled.
Nonce manager, error types, matchHTU, and authenticator metadata
service/internal/auth/authn.go
Adds dpopNonceManager with atomic current/previous nonce rotation and expiration; exports DPoPNonceError and DPoPNonceMalformedError; adds matchHTU for strict/loose HTU matching; registers dpop_signing_alg_values_supported and dpop_nonce_required in well-known endpoint.
HTTP and Connect nonce challenge propagation with token validation
service/internal/auth/authn.go
Updates HTTP mux and Connect interceptor to build HTU/HTM receiver expectations, parse DPoP/Bearer authorization scheme, invoke matchHTU, issue DPoP-Nonce + WWW-Authenticate challenge headers on DPoPNonceError, attach fresh DPoP-Nonce on success, warn on cnf/Bearer mismatch, and validate nonce claims in validateDPoP.
HTTP method propagation and HTU matching tests
service/internal/auth/authn_test.go
Adds HTTPMethod() to test request helper; adds tests for HTM mismatch rejection, checkToken GET acceptance, Connect interceptor method forwarding, and table-driven matchHTU strict/loose behavior.
Nonce manager and nonce error integration tests
service/internal/auth/dpop_nonce_test.go
Adds unit tests for nonce generation, rotation, acceptance window, and eviction; error type string/errors.As coverage; algorithm restriction assertions; and AuthSuite integration tests for missing, valid, malformed, and mismatched nonce scenarios in checkToken.
SDK-side DPoP logging
sdk/auth/oauth/oauth.go, sdk/auth/token_adding_interceptor.go
Adds slog.Debug calls in getDPoPAssertion, AddCredentials, AddCredentialsConnect, and GetDPoPToken to record htm, htu, nonce presence, and stream type during DPoP token construction.
KAS access error classification and rewrap response mapping
service/kas/access/accessPdp.go, service/kas/access/rewrap.go
Adds classifyAccessError mapping context cancellations and Connect transport codes to stable categories; updates checkAttributes to log structured category/code fields; changes tdf3Rewrap canAccess failure path to return err403 for denied vs err500 for internal errors; reformats surrounding warn/debug log call sites.

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: receiverInfo{u:[http://host/path, https://host/path], m:[POST]}
  checkToken->>validateDPoP: token, dpopProof, receiverInfo
  validateDPoP->>dpopNonceManager: validate(nonceClaim)
  alt Missing or expired nonce
    dpopNonceManager-->>validateDPoP: DPoPNonceError
    validateDPoP-->>MuxHandler: DPoPNonceError
    MuxHandler-->>Client: 401 + DPoP-Nonce: <fresh> + WWW-Authenticate: DPoP error="use_dpop_nonce"
  else Malformed nonce type
    dpopNonceManager-->>validateDPoP: DPoPNonceMalformedError
    validateDPoP-->>MuxHandler: DPoPNonceMalformedError
    MuxHandler-->>Client: 401 (no retry challenge)
  else Valid nonce
    dpopNonceManager-->>validateDPoP: nil
    validateDPoP-->>checkToken: claims
    checkToken-->>MuxHandler: claims
    MuxHandler-->>Client: 200 + DPoP-Nonce: <rotated>
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • opentdf/platform#3479: Modifies service/internal/auth/authn.go around DPoP htu/receiver URI validation by removing lookupOrigins/lookupGatewayPaths and changing how receiverInfo.u is set, directly connected to this PR's HTU matching and receiver URI construction changes.

Suggested labels

comp:kas, comp:authorization

Suggested reviewers

  • strantalis
  • jakedoublev
  • pflynn-virtru

Poem

🐇 Hop hop, the nonce goes round and round,
A DPoP challenge — retryable, sound!
Strict HTU or loose, we pick the mode,
Keycloak 26.2 joins the node.
503? 403? We classify the blame —
This bunny's auth stack will never be the same! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% 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 PR title accurately and concisely summarizes the main change: adding comprehensive DPoP (RFC 9449) support, which aligns with the extensive changes across multiple files implementing DPoP proof validation, nonce challenges, and token scheme support.
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

dmihalcik-virtru and others added 18 commits June 22, 2026 13:53
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>
Signed-off-by: Dave Mihalcik <dmihalcik@virtru.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>
Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
Signed-off-by: Dave Mihalcik <dmihalcik@virtru.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>
Signed-off-by: Dave Mihalcik <dmihalcik@virtru.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>
Signed-off-by: Dave Mihalcik <dmihalcik@virtru.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>
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>
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>
Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
…-ref

- Pin StrictHTU:false in nonce test fixture for deterministic nonce tests
- Use full-origin htu in GET DPoP test to decouple from loose HTU matching
- Use ${{ inputs.platform-ref }} instead of hardcoded feat-kc26-dpop tag in test action

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
Revert switch to platform-ref; prefer a stricter explicit pin for the downloaded scripts/compose.

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
Keep the default docker-compose on Keycloak 25 so downstream consumers that
aren't DPoP-ready stay on it. The start-up action now overlays Keycloak 26.2
plus the admin-fine-grained-authz:v1 feature flag onto docker-compose only when
dpop-challenge-enabled=true, so DPoP/nonce testing runs against 26.2 without
forcing the bump on everyone. Stop overwriting docker-compose.yaml from the
feature tag so the checked-out (KC 25) compose is the base.

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
Add a server-side replay cache that rejects reused DPoP proof jti values
within the acceptance window (RFC 9449 §11.1). The check runs as the last
step in validateDPoP so only otherwise-valid proofs populate the cache,
and the cache TTL tracks DPoPSkew since older proofs are already rejected
by the iat freshness check.

Also addresses review nits: move the dpopNonceManager doc comment onto the
correct type and use WarnContext in checkToken.

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
@dmihalcik-virtru dmihalcik-virtru force-pushed the DSPX-3397-platform-service branch from b28062c to e3489bf Compare June 22, 2026 17:53
@policy-bot-opentdf policy-bot-opentdf Bot dismissed elizabethhealy’s stale review June 22, 2026 17:53

Invalidated by push of e3489bf

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

Benchmark authorization.v2.GetMultiResourceDecision Results:

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

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.957251ms
Throughput 240.99 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 42.134381383s
Average Latency 419.146717ms
Throughput 118.67 requests/second

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

@dmihalcik-virtru dmihalcik-virtru added this pull request to the merge queue Jun 22, 2026
Merged via the queue into main with commit 8a00646 Jun 22, 2026
39 checks passed
@dmihalcik-virtru dmihalcik-virtru deleted the DSPX-3397-platform-service branch June 22, 2026 21:00
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.

3 participants