Skip to content

feat(sdk): DSPX-5640 negotiate DPoP htu style from server advertisement - #441

Draft
dmihalcik-virtru wants to merge 1 commit into
mainfrom
DSPX-5640-java-sdk-dpop-negotiation
Draft

dmihalcik-virtru wants to merge 1 commit into
mainfrom
DSPX-5640-java-sdk-dpop-negotiation

Conversation

@dmihalcik-virtru

@dmihalcik-virtru dmihalcik-virtru commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Proposed Changes

Fixes DSPX-5640. Part of epic DSPX-5549 (DPoP RFC 9449 migration, step 2.1). The Go equivalent is opentdf/platform#4322 (DSPX-5639); both SDKs follow the same rules.

Problem. The SDK always signs DPoP proofs with the full request URL as htu. The first DPoP servers (service v0.11.x, shipped in DSP 2.7.x) compare an RPC's htu against its procedure path, so they reject every request from a current Java client.

Fix. Pick the htu style per server origin from what the server advertises.

  • Discovery, unauthenticated, once per origin:
    1. GET /.well-known/opentdf-configuration: a dpop block's rfc9449 is authoritative; otherwise a non-empty top-level dpop_signing_alg_values_supported means RFC 9449 (service v0.18.0 and later);
    2. otherwise RFC 9728 /.well-known/oauth-protected-resource with dpop_signing_alg_values_supported;
    3. otherwise legacy.
  • Caching: only definitive answers are cached. A network error or 5xx on the well-known step uses legacy for that request only and is retried next time, so a blip can't pin a modern server to legacy. The platform origin is seeded from the builder's existing well-known fetch when it advertises, so the common case makes no extra requests.
  • Legacy changes only the htu, to the procedure (/pkg.Service/Method, the last two path segments, dropping any proxy prefix). The DPoP Authorization scheme, ath and nonce handling are unchanged: step-One servers accept the DPoP scheme, and strict_scheme servers require it. Non-RPC requests and token-endpoint proofs keep the full URI. Plain Bearer tokens are unaffected.
  • Override: SDKBuilder.dpopMode(DPoPMode.AUTO | LEGACY | RFC9449), default AUTO. The existing public TokenSource constructor stays fixed to RFC 9449.

Testing

  • mvn -pl sdk -am test: 459 tests, 0 failures, 9 skipped (the existing FIPS and EC skips).
  • New DPoPModeTest (21 tests): each advertisement source, both, neither; transient failures not cached and rediscovered; definitive 404 and unparseable answers cached; bootstrap seeding; both overrides; proxy-prefix, non-RPC and short-path htu; a nonce challenge in each mode.
  • SDKBuilderTest now runs in AUTO. A seeded platform signs the full URL, while a KAS with no well-known documents gets the procedure htu, both under the DPoP scheme.
  • Cross-version end-to-end coverage goes in opentdf/tests xtest: DSPX-5642.

Checklist

  • I have added or updated unit tests
  • I have added or updated integration tests (if appropriate)
  • I have added or updated documentation

The SDK always signed DPoP proofs with the full request URL as htu.
The first DPoP servers (service v0.11.x, DSP 2.7.x) compare an RPC's
htu against its procedure and reject a full URL, so a new Java client
could not talk to an old server.

Pick the htu style per server origin, mirroring the Go SDK
(sdk/auth/dpop_mode.go):
1. unauthenticated GET /.well-known/opentdf-configuration; a dpop
   block's rfc9449 is authoritative, else a non-empty top-level
   dpop_signing_alg_values_supported means RFC 9449 (service v0.18.0+),
2. else RFC 9728 /.well-known/oauth-protected-resource with that key,
3. else legacy.
Only definitive answers are cached: a non-5xx answer from the opentdf
well-known (a 4xx or an unparseable 200 means not advertised) or an
RFC 9728 advertisement. A network error or 5xx on the well-known step
uses legacy for that request only, logs a warning, and is retried on
the next request. The platform origin is seeded from the bootstrap
well-known response when it advertises, so the common case makes no
extra requests.

Legacy keeps the DPoP Authorization scheme, ath and nonce handling;
only the htu changes, to the procedure (/pkg.Service/Method, the last
two path segments, dropping any proxy prefix). Non-RPC requests and
token endpoint proofs keep the full URI, and non-DPoP Bearer tokens are
unaffected. SDKBuilder.dpopMode(AUTO | LEGACY | RFC9449; default AUTO)
overrides discovery, matching Go's WithDPoPMode.

Verified with mvn -pl sdk -am test (459 tests, 0 failures). DPoPModeTest
covers each advertisement source, both, neither, transient failures not
being cached, definitive 404 and unparseable answers being cached,
bootstrap seeding, the override, and a nonce challenge in each mode.
SDKBuilderTest now checks a seeded platform signs the full URL while a
KAS with no well-known documents gets the procedure htu.

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

coderabbitai Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@github-actions

Copy link
Copy Markdown
Contributor

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant