Skip to content

feat(sdk): deprecate AuthProvider in favor of Interceptor pattern#899

Merged
eugenioenko merged 4 commits into
mainfrom
feat/interceptor-auth-pattern
Apr 1, 2026
Merged

feat(sdk): deprecate AuthProvider in favor of Interceptor pattern#899
eugenioenko merged 4 commits into
mainfrom
feat/interceptor-auth-pattern

Conversation

@eugenioenko
Copy link
Copy Markdown
Contributor

@eugenioenko eugenioenko commented Mar 31, 2026

Unifies auth for both PlatformClient and OpenTDF on the Connect RPC Interceptor pattern, eliminating the updateClientPublicKey footgun and the dual-abstraction complexity before 1.0.

  • Add authTokenInterceptor, authTokenDPoPInterceptor, authProviderInterceptor helpers
  • Introduce AuthConfig union type (AuthProvider | { interceptors }) throughout
  • OpenTDF and PlatformClient accept interceptors as the primary auth mechanism
  • Thread interceptors through TDF3Client, tdf.ts, access, and policy layers
  • AuthProvider remains fully supported as backwards-compatible (deprecated)
  • Legacy fetch fallback preserved when AuthProvider is used
  • Update README with interceptor-first examples

Discussion: https://github.com/orgs/opentdf/discussions/3167

What we gain:

  • Single auth pattern — both PlatformClient and OpenTDF use interceptors, no more two mental models
  • Kill the updateClientPublicKey footgun — no more hidden ordering requirement where you must await client.ready before PlatformClient works
  • DPoP becomes explicit — opt-in via authTokenDPoPInterceptor() instead of a side effect buried in AccessToken internals
  • Remove the bridge layer — createAuthInterceptor (the AuthProvider-to-Interceptor wrapper in platform.ts) goes away as production code, only needed for backwards compat
  • Simpler standalone PlatformClient usage — no need to construct an OpenTDF first just to get auth working
  • Bring-your-own-auth becomes trivial — users behind a proxy or with custom auth just write a 3-line interceptor, no need to implement the AuthProvider interface
  • Smaller public API surface for 1.0 — fewer types to commit to supporting forever
  • Aligns with Connect RPC idioms — interceptors are the native pattern for @connectrpc/connect-web, so we stop fighting the framework
  • Testability — interceptors are plain functions, easier to test and mock than stateful AuthProvider instances with internal token caches

Summary by CodeRabbit

  • New Features

    • Interceptor-based auth is now supported across SDK surfaces; token and DPoP interceptors exported and preferred. Legacy auth-provider path is deprecated but retained for compatibility.
  • Documentation

    • README updated with “With Interceptors (Recommended)” examples, DPoP usage, custom-interceptor guidance, and legacy approach moved under “(Legacy)”.
  • Tests

    • Added tests for token & DPoP interceptors, DPoP proof contents, and interceptor-only client behavior.

@eugenioenko eugenioenko requested a review from a team as a code owner March 31, 2026 20:15
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 31, 2026

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 ConnectRPC interceptor-based authentication: new token and DPoP interceptors, an AuthConfig union and resolvers, wiring interceptors across Platform/TDF3 clients and RPC layers, README examples updated to show interceptor usage, and tests exercising both interceptor and legacy AuthProvider flows.

Changes

Cohort / File(s) Summary
Auth interceptor utilities
lib/src/auth/interceptors.ts
New module: TokenProvider, DPoP types; authTokenInterceptor, authTokenDPoPInterceptor, authProviderInterceptor; AuthConfig union plus isInterceptorConfig, resolveInterceptors, resolveAuthConfig.
Library exports
lib/src/index.ts
Re-exported interceptor factories and related types (authTokenInterceptor, authTokenDPoPInterceptor, authProviderInterceptor, AuthConfig, DPoPInterceptor, DPoPInterceptorOptions, TokenProvider).
OpenTDF & README
lib/src/opentdf.ts, lib/README.md
OpenTDF accepts interceptors?: Interceptor[]; authProvider made optional/deprecated; constructor/ready behavior updated for interceptor-only mode; README updated with interceptor and DPoP examples and legacy AuthProvider moved to legacy section.
Platform client & RPC access
lib/src/platform.ts, lib/src/access/access-rpc.ts, lib/src/access.ts
APIs switched to accept AuthConfig; PlatformClient now constructed with resolveInterceptors(auth); RPC path uses interceptors; legacy authProvider supported via authProviderInterceptor and only used as a fallback when present.
Policy & discovery APIs
lib/src/policy/api.ts, lib/src/policy/discovery.ts
Public functions now take auth: AuthConfig instead of authProvider; PlatformClient construction updated to use resolveInterceptors(auth) when auth is provided.
TDF3 client & TDF APIs
lib/tdf3/src/client/index.ts, lib/tdf3/src/tdf.ts
Client/ClientConfig accept interceptors?: Interceptor[]; Client.auth (AuthConfig) replaces authProvider; encrypt/decrypt configs accept auth?: AuthConfig; unwrapKey and related flows accept/resolve auth.
Platform internals cleanup
lib/src/platform.ts
Removed local createAuthInterceptor; replaced with shared authProviderInterceptor(options.authProvider); authProvider marked deprecated in JSDoc.
Tests
lib/tests/web/interceptors.test.ts, lib/tests/web/opentdf.test.ts, lib/tests/web/platform-rpc.test.ts
New/updated tests for token and DPoP interceptors (headers, JWT structure, dpopKeys), authProvider bridge behavior, auth config resolvers, OpenTDF constructor with interceptors, and PlatformClient RPC calls using interceptors.
Docs & examples
lib/README.md
Primary README examples updated to show interceptor usage and DPoP example; custom Interceptor guidance added; legacy AuthProvider example moved under a legacy section.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Interceptor as authTokenInterceptor
    participant TokenProvider
    participant Transport as RequestHandler

    Client->>Interceptor: send RPC request
    Interceptor->>TokenProvider: await token()
    TokenProvider-->>Interceptor: token
    Interceptor->>Interceptor: set Authorization: Bearer <token>
    Interceptor->>Transport: next(req)
    Transport-->>Client: response
Loading
sequenceDiagram
    participant Client
    participant DPoPInterceptor
    participant TokenProvider
    participant Crypto as CryptoService
    participant DPoPUtils
    participant Transport as RequestHandler

    Client->>DPoPInterceptor: send RPC request
    par resolve token and keys
        DPoPInterceptor->>TokenProvider: await token()
        DPoPInterceptor->>Crypto: await dpopKeys
    end
    TokenProvider-->>DPoPInterceptor: token
    Crypto-->>DPoPInterceptor: keyPair
    DPoPInterceptor->>DPoPUtils: generate DPoP proof (keys, method, url)
    DPoPUtils-->>DPoPInterceptor: signed JWT
    DPoPInterceptor->>DPoPInterceptor: set Authorization, DPoP, X-VirtruPubKey headers
    DPoPInterceptor->>Transport: next(req)
    Transport-->>Client: response
Loading
sequenceDiagram
    participant OpenTDF
    participant AuthConfig
    participant Resolver as resolveInterceptors
    participant PlatformClient
    participant RPC

    OpenTDF->>AuthConfig: provide { interceptors } or AuthProvider
    AuthConfig->>Resolver: resolveInterceptors(auth)
    Resolver-->>OpenTDF: interceptor[]
    OpenTDF->>OpenTDF: store interceptors, set ready (if interceptors-only)
    OpenTDF->>PlatformClient: construct with interceptors
    PlatformClient->>RPC: intercepted request -> transport
    RPC-->>PlatformClient: response
    PlatformClient-->>OpenTDF: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I nibbled headers, seeded tokens in flight,
Interceptors hum, DPoP proofs take flight.
Legacy tucked neatly, new paths hop clear,
A rabbit’s small cheer for auth brought near.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: deprecating AuthProvider in favor of the Interceptor pattern, which is the central objective of this PR.

✏️ 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 feat/interceptor-auth-pattern

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

@github-actions
Copy link
Copy Markdown

If these changes look good, signoff on them with:

git pull && git commit --amend --signoff && git push --force-with-lease origin

If they aren't any good, please remove them with:

git pull && git reset --hard HEAD~1 && git push --force-with-lease origin

@eugenioenko eugenioenko force-pushed the feat/interceptor-auth-pattern branch from 94b4d55 to 0e2ea76 Compare March 31, 2026 20:16
Copy link
Copy Markdown

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

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 a new authentication pattern using Connect RPC interceptors, offering a more flexible alternative to the legacy AuthProvider. It adds factory functions for bearer and DPoP-bound token interceptors, updates core clients (OpenTDF, PlatformClient, TDF3Client) to support this pattern, and provides a bridge for backward compatibility. Review feedback identifies a critical issue in lib/src/access.ts where RPC errors are masked by generic fallback error messages when an authProvider is absent. There is also a suggestion to broaden the TokenProvider type to allow synchronous string returns for better flexibility.

Comment thread lib/src/access.ts
Comment thread lib/src/access.ts Outdated
Comment thread lib/src/auth/interceptors.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
lib/src/policy/discovery.ts (1)

38-39: ⚠️ Potential issue | 🟡 Minor

Stale JSDoc: parameter name changed from authProvider to auth.

The documentation still references authProvider but the parameter is now auth: AuthConfig. Same issue appears at lines 93-94, 154, and 194.

📝 Suggested fix
  * `@param` platformUrl The platform base URL.
- * `@param` authProvider An auth provider for the request.
+ * `@param` auth Auth configuration (AuthProvider or { interceptors }) for the request.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/policy/discovery.ts` around lines 38 - 39, Summary: Stale JSDoc param
name: replace authProvider with auth. Update every JSDoc `@param` tag that still
says "authProvider" to use the actual parameter name "auth" and the correct type
AuthConfig, e.g. change "@param authProvider ..." to "@param auth {AuthConfig}
...", keeping the original description; do this for all occurrences in this
module (the functions that accept the auth parameter), run typecheck/lint to
ensure JSDoc and signatures match, and adjust any related `@returns` or inline
docs if they reference the old name.
🧹 Nitpick comments (2)
lib/src/opentdf.ts (1)

351-353: Dead code: the final else branch is unreachable.

Given the validation at lines 307-309, by the time we reach line 337:

  • If interceptors has elements and authProvider is absent → branch at 337
  • If authProvider is present → branch at 341
  • If neither has valid auth → constructor already threw

The else at line 351 can never execute.

🔧 Remove unreachable branch
     if (interceptors && !authProvider) {
       // Interceptor path: no updateClientPublicKey needed.
       // DPoP key binding is handled by the interceptor itself.
       this.ready = Promise.resolve();
-    } else if (authProvider) {
+    } else {
       // Legacy AuthProvider path: eagerly bind DPoP keys so PlatformClient
       // can make gRPC calls without waiting for a TDF operation first.
       this.ready = this.dpopEnabled
         ? this.dpopKeys.then((keys) => authProvider.updateClientPublicKey(keys))
         : Promise.resolve();
       // Prevent unhandled rejection if caller doesn't await ready.
       this.ready.catch((err) => {
         console.warn('OpenTDF: DPoP key binding failed during initialization:', err);
       });
-    } else {
-      this.ready = Promise.resolve();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/opentdf.ts` around lines 351 - 353, The final unreachable else branch
that sets this.ready = Promise.resolve() should be removed from the constructor:
locate the constructor in opentdf.ts where this.ready is assigned (references to
this.ready, interceptors, and authProvider) and delete the unreachable else
block; keep the two valid branches that set this.ready when interceptors exist
or when authProvider is provided, and ensure no other code expects the removed
branch.
lib/src/auth/interceptors.ts (1)

137-148: Consider more robust error detection for DPoP binding failures.

The heuristic checking for 'public key' or 'updateClientPublicKey' in error messages could miss variations or be triggered by unrelated errors. This is acceptable for backwards-compatibility guidance but is inherently fragile.

💡 Alternative: Use error type/code if available

If the underlying auth providers throw typed errors (e.g., a custom DPoPBindingError), checking instanceof would be more reliable than string matching. However, if the existing providers only throw generic Error, the current approach is pragmatic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/auth/interceptors.ts` around lines 137 - 148, Replace the fragile
string-only checks in the catch block with a small robust predicate (e.g.,
isDPoPBindingError) and use it to detect DPoP binding failures: implement
isDPoPBindingError(err) to first check typed indicators (err instanceof
DPoPBindingError or err.name === 'DPoPBindingError' or err.code ===
'DPoP_BINDING_FAILED'), then fall back to a conservative regex/test on the
message (matching "public key" or "updateClientPublicKey") for backwards
compatibility; call this predicate in the existing catch and keep the same
thrown Error text when true, otherwise rethrow the original err.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@lib/src/policy/discovery.ts`:
- Around line 38-39: Summary: Stale JSDoc param name: replace authProvider with
auth. Update every JSDoc `@param` tag that still says "authProvider" to use the
actual parameter name "auth" and the correct type AuthConfig, e.g. change
"@param authProvider ..." to "@param auth {AuthConfig} ...", keeping the
original description; do this for all occurrences in this module (the functions
that accept the auth parameter), run typecheck/lint to ensure JSDoc and
signatures match, and adjust any related `@returns` or inline docs if they
reference the old name.

---

Nitpick comments:
In `@lib/src/auth/interceptors.ts`:
- Around line 137-148: Replace the fragile string-only checks in the catch block
with a small robust predicate (e.g., isDPoPBindingError) and use it to detect
DPoP binding failures: implement isDPoPBindingError(err) to first check typed
indicators (err instanceof DPoPBindingError or err.name === 'DPoPBindingError'
or err.code === 'DPoP_BINDING_FAILED'), then fall back to a conservative
regex/test on the message (matching "public key" or "updateClientPublicKey") for
backwards compatibility; call this predicate in the existing catch and keep the
same thrown Error text when true, otherwise rethrow the original err.

In `@lib/src/opentdf.ts`:
- Around line 351-353: The final unreachable else branch that sets this.ready =
Promise.resolve() should be removed from the constructor: locate the constructor
in opentdf.ts where this.ready is assigned (references to this.ready,
interceptors, and authProvider) and delete the unreachable else block; keep the
two valid branches that set this.ready when interceptors exist or when
authProvider is provided, and ensure no other code expects the removed branch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: da473a10-8b9f-49df-abb3-2bcb202eb352

📥 Commits

Reviewing files that changed from the base of the PR and between 6148c12 and 0e2ea76.

📒 Files selected for processing (14)
  • lib/README.md
  • lib/src/access.ts
  • lib/src/access/access-rpc.ts
  • lib/src/auth/interceptors.ts
  • lib/src/index.ts
  • lib/src/opentdf.ts
  • lib/src/platform.ts
  • lib/src/policy/api.ts
  • lib/src/policy/discovery.ts
  • lib/tdf3/src/client/index.ts
  • lib/tdf3/src/tdf.ts
  • lib/tests/web/interceptors.test.ts
  • lib/tests/web/opentdf.test.ts
  • lib/tests/web/platform-rpc.test.ts

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR migrates the SDK’s authentication model toward Connect RPC interceptors (with DPoP support) while keeping the legacy AuthProvider supported (deprecated). It threads the new AuthConfig shape through Platform/OpenTDF/TDF3 client layers, updates RPC/legacy fallbacks accordingly, and expands docs/tests to cover interceptor-first usage.

Changes:

  • Introduces authTokenInterceptor, authTokenDPoPInterceptor, authProviderInterceptor, plus AuthConfig + resolver utilities.
  • Updates PlatformClient/OpenTDF/TDF3 client and access/policy layers to accept interceptor-based auth as the preferred path (AuthProvider remains supported but deprecated).
  • Adds/updates web tests and README examples to validate the interceptor path.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
lib/src/auth/interceptors.ts New interceptor helpers + AuthConfig utilities; core of the interceptor auth feature.
lib/src/platform.ts Switches legacy AuthProvider handling to the new authProviderInterceptor.
lib/src/opentdf.ts Adds interceptor-first constructor options and readiness behavior.
lib/tdf3/src/client/index.ts Adds interceptor support to TDF3 Client config and exposes a unified auth getter.
lib/tdf3/src/tdf.ts Threads auth?: AuthConfig through encrypt/decrypt config and rewrap flow.
lib/src/access/access-rpc.ts Updates RPC access helpers to accept AuthConfig and resolve interceptors.
lib/src/access.ts Updates hybrid RPC/legacy fallback logic to use AuthConfig + resolveAuthConfig.
lib/src/policy/api.ts Switches policy API functions to AuthConfig and resolves interceptors for PlatformClient.
lib/src/policy/discovery.ts Same as above for discovery helpers.
lib/src/index.ts Exports the new interceptor helpers/types from the public SDK entrypoint.
lib/tests/web/interceptors.test.ts New test coverage for interceptor helpers and auth config utilities.
lib/tests/web/platform-rpc.test.ts Adds PlatformClient integration tests for interceptor-only auth.
lib/tests/web/opentdf.test.ts Adds OpenTDF constructor tests for interceptor-only auth.
lib/README.md Updates docs to interceptor-first guidance with legacy AuthProvider section.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/src/auth/interceptors.ts
Comment thread lib/src/auth/interceptors.ts Outdated
Comment thread lib/src/access.ts
Comment thread lib/src/access.ts
Comment thread lib/tdf3/src/client/index.ts
Comment thread lib/tdf3/src/client/index.ts Outdated
Comment thread lib/tests/web/interceptors.test.ts Outdated
Comment thread lib/src/platform.ts Outdated
Comment thread lib/src/opentdf.ts Outdated
@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

If these changes look good, signoff on them with:

git pull && git commit --amend --signoff && git push --force-with-lease origin

If they aren't any good, please remove them with:

git pull && git reset --hard HEAD~1 && git push --force-with-lease origin

@github-actions
Copy link
Copy Markdown

X-Test Failure Report

opentdf-ctl
opentdf-sdk-lib

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
lib/tests/web/interceptors.test.ts (1)

128-143: Assert the request passed into withCreds(), not just the returned headers.

This test would not catch regressions in the bridge input shape, which is the risky part of the legacy-compat adapter. Please capture the HttpRequest argument and pin at least url and method here so a lossy adapter cannot still pass the suite.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/tests/web/interceptors.test.ts` around lines 128 - 143, Update the test
that uses mockAuthProvider/withCreds and captureHeaders to also capture the
HttpRequest argument passed into withCreds (the request parameter to the mock
withCreds function) and assert at minimum its url and method properties
alongside the existing header assertions; locate the mockAuthProvider.withCreds
in the test, store the received HttpRequest (type HttpRequest) into a variable
or spy when called by authProviderInterceptor, then add expect checks for
request.url and request.method to ensure the bridge input shape is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/src/auth/interceptors.ts`:
- Around line 123-136: The code currently strips the request URL to pathOnly and
passes that to authProvider.withCreds, which loses origin/query info; update the
call in interceptors.ts to forward the full request URL (use req.url or
url.href) instead of pathOnly when calling authProvider.withCreds so the legacy
AuthProvider receives the entire HttpRequest URL for signature generation,
keeping the same method and headers handling around the withCreds call
(referencing authProvider.withCreds, req.url, url, and pathOnly to locate the
change).

In `@lib/src/opentdf.ts`:
- Around line 325-330: When constructing OpenTDF ensure you reuse any DPoP
keypair exposed by an auth interceptor before generating a new one: check for a
provided interceptor from authTokenDPoPInterceptor() (inspect
interceptor.dpopKeys) and if present assign this.dpopKeys =
interceptor.dpopKeys; otherwise fall back to
this.cryptoService.generateSigningKeyPair(); then pass this.dpopKeys into new
TDF3Client (dpopEnabled ? this.dpopKeys : undefined). Apply the same resolution
logic replacing the mirrored fallback in lib/tdf3/src/client/index.ts so both
places derive keys from interceptor.dpopKeys first.

---

Nitpick comments:
In `@lib/tests/web/interceptors.test.ts`:
- Around line 128-143: Update the test that uses mockAuthProvider/withCreds and
captureHeaders to also capture the HttpRequest argument passed into withCreds
(the request parameter to the mock withCreds function) and assert at minimum its
url and method properties alongside the existing header assertions; locate the
mockAuthProvider.withCreds in the test, store the received HttpRequest (type
HttpRequest) into a variable or spy when called by authProviderInterceptor, then
add expect checks for request.url and request.method to ensure the bridge input
shape is preserved.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 48289cf7-e755-4e48-a8bb-5aa5a0ba35bb

📥 Commits

Reviewing files that changed from the base of the PR and between 0e2ea76 and 445a65c.

📒 Files selected for processing (6)
  • lib/src/access.ts
  • lib/src/auth/interceptors.ts
  • lib/src/opentdf.ts
  • lib/src/platform.ts
  • lib/tdf3/src/client/index.ts
  • lib/tests/web/interceptors.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/src/access.ts

Comment thread lib/src/auth/interceptors.ts
Comment thread lib/src/opentdf.ts
@eugenioenko eugenioenko force-pushed the feat/interceptor-auth-pattern branch 3 times, most recently from b9edbb9 to 70b18aa Compare March 31, 2026 22:07
@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

X-Test Failure Report

opentdf-ctl
opentdf-sdk-lib

@github-actions
Copy link
Copy Markdown

X-Test Failure Report

opentdf-ctl
opentdf-sdk-lib

Comment thread lib/src/auth/interceptors.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
lib/tests/web/interceptors.test.ts (1)

145-160: Minor: Test description doesn't match implementation.

The test is named "wraps updateClientPublicKey errors" but actually tests error wrapping when withCreds throws. This is technically correct behavior (the implementation catches any error containing "public key"), but the test name is misleading.

📝 Suggested rename
-  it('wraps updateClientPublicKey errors with helpful message', async () => {
+  it('wraps withCreds errors mentioning public key with helpful message', async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/tests/web/interceptors.test.ts` around lines 145 - 160, Rename the test
description to accurately reflect what it's asserting: change the "wraps
updateClientPublicKey errors with helpful message" title to mention withCreds
(e.g., "wraps withCreds errors with helpful message" or similar) so it matches
the failingProvider implementation where updateClientPublicKey is a no-op and
withCreds throws, and keep the rest of the test (authProviderInterceptor,
failingProvider, captureHeaders) unchanged.
lib/src/auth/interceptors.ts (1)

137-148: Consider: Fragile error detection via string matching.

The error detection relies on checking if the message includes "public key" or "updateClientPublicKey". This could miss errors with different wording or match unrelated errors. However, since this is a backwards-compatibility bridge that will eventually be deprecated, this pragmatic approach is acceptable.

Based on learnings: authProviderInterceptor intentionally passes only url.pathname (not the full URL) to authProvider.withCreds() to preserve backwards-compatibility with legacy AuthProvider implementations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/auth/interceptors.ts` around lines 137 - 148, The catch block in
authProviderInterceptor currently detects DPoP/PK errors by fragile
case-sensitive substring checks on msg (using includes('public key') or
'updateClientPublicKey'); update this handling to use a case-insensitive match
(e.g., check msg.toLowerCase()) for those substrings to reduce false negatives,
and add a clear comment near the catch explaining that authProviderInterceptor
intentionally passes only url.pathname to authProvider.withCreds() to preserve
backwards-compatibility with legacy AuthProvider implementations (so do not
change the pathname behavior). Ensure you update the logic around the thrown
Error message in the catch (the code that throws the 'PlatformClient: DPoP key
binding is not complete...' error) to use the normalized/message check and keep
the original error text included.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/src/auth/interceptors.ts`:
- Around line 137-148: The catch block in authProviderInterceptor currently
detects DPoP/PK errors by fragile case-sensitive substring checks on msg (using
includes('public key') or 'updateClientPublicKey'); update this handling to use
a case-insensitive match (e.g., check msg.toLowerCase()) for those substrings to
reduce false negatives, and add a clear comment near the catch explaining that
authProviderInterceptor intentionally passes only url.pathname to
authProvider.withCreds() to preserve backwards-compatibility with legacy
AuthProvider implementations (so do not change the pathname behavior). Ensure
you update the logic around the thrown Error message in the catch (the code that
throws the 'PlatformClient: DPoP key binding is not complete...' error) to use
the normalized/message check and keep the original error text included.

In `@lib/tests/web/interceptors.test.ts`:
- Around line 145-160: Rename the test description to accurately reflect what
it's asserting: change the "wraps updateClientPublicKey errors with helpful
message" title to mention withCreds (e.g., "wraps withCreds errors with helpful
message" or similar) so it matches the failingProvider implementation where
updateClientPublicKey is a no-op and withCreds throws, and keep the rest of the
test (authProviderInterceptor, failingProvider, captureHeaders) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dab6ee54-b1aa-4060-ab5d-d646526df503

📥 Commits

Reviewing files that changed from the base of the PR and between 445a65c and 70b18aa.

📒 Files selected for processing (14)
  • lib/README.md
  • lib/src/access.ts
  • lib/src/access/access-rpc.ts
  • lib/src/auth/interceptors.ts
  • lib/src/index.ts
  • lib/src/opentdf.ts
  • lib/src/platform.ts
  • lib/src/policy/api.ts
  • lib/src/policy/discovery.ts
  • lib/tdf3/src/client/index.ts
  • lib/tdf3/src/tdf.ts
  • lib/tests/web/interceptors.test.ts
  • lib/tests/web/opentdf.test.ts
  • lib/tests/web/platform-rpc.test.ts
✅ Files skipped from review due to trivial changes (2)
  • lib/src/index.ts
  • lib/tests/web/opentdf.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/tdf3/src/tdf.ts
  • lib/src/policy/discovery.ts

Unifies auth for both PlatformClient and OpenTDF on the Connect RPC
Interceptor pattern, eliminating the updateClientPublicKey footgun and
the dual-abstraction complexity before 1.0.

- Add authTokenInterceptor, authTokenDPoPInterceptor, authProviderInterceptor helpers
- Introduce AuthConfig union type (AuthProvider | { interceptors }) throughout
- OpenTDF and PlatformClient accept interceptors as the primary auth mechanism
- Thread interceptors through TDF3Client, tdf.ts, access, and policy layers
- AuthProvider remains fully supported as backwards-compatible (deprecated)
- Legacy fetch fallback preserved when AuthProvider is used
- Skip legacy fallback when no AuthProvider to propagate RPC errors directly
- Use full URI for DPoP htu claim, base64.encode for X-VirtruPubKey header
- Update README with interceptor-first examples

Discussion: https://github.com/orgs/opentdf/discussions/3167

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@eugenioenko eugenioenko force-pushed the feat/interceptor-auth-pattern branch from 70b18aa to 4337118 Compare March 31, 2026 22:20
@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

Comment thread lib/src/auth/interceptors.ts
Copy link
Copy Markdown
Contributor

@marythought marythought left a comment

Choose a reason for hiding this comment

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

Overall: This is a solid DX improvement. The interceptor pattern is genuinely simpler than AuthProvider for most users, kills the updateClientPublicKey / await client.ready footgun, and aligns with Connect RPC idioms. A few notes:

Docs follow-up

This PR marks AuthProvider as deprecated and the README now leads with interceptors as the recommended pattern. The published docs at opentdf.io (quickstart, discovery, tdf, authorization, policy, authentication, platform-client, troubleshooting) all use AuthProvider exclusively in JS examples — we should coordinate a docs update as a follow-up to this PR. When we do, I'd suggest leading with a "define once, pass everywhere" pattern:

import { authTokenInterceptor, OpenTDF, listAttributes, attributeExists } from '@opentdf/sdk';

// Define once
const auth = { interceptors: [authTokenInterceptor(() => getAccessToken())] };

// Reuse
const client = new OpenTDF({ ...auth, platformUrl });
const attrs = await listAttributes(platformUrl, auth);
const exists = await attributeExists(platformUrl, auth, 'https://opentdf.io/attr/department');

Deprecation version

authProvider is marked @deprecated but without a version timeline. Adding something like @deprecated Since 0.x. Will be removed in 1.0. to the JSDoc would help developers plan their migration.

Dual auth + authProvider threading

After this PR, both auth: AuthConfig and authProvider: AuthProvider are passed side-by-side through multiple internal layers, and each layer has to decide which one to use. Concrete examples:

  • EncryptConfiguration and DecryptConfiguration in tdf.ts both carry auth?: AuthConfig and authProvider?: AuthProvider as separate fields
  • unwrapKey in tdf.ts accepts both, then resolves with const resolvedAuth: AuthConfig = (auth ?? authProvider) as AuthConfig — the type assertion papers over the ambiguity
  • Client in client/index.ts stores both authProvider and interceptors as separate fields, then has a get auth() getter that resolves which one to use
  • When calling downstream functions, both get passed: auth: this.auth, authProvider: this.authProvider

This means every internal function has to reason about priority between two auth fields that represent the same concept, and the pattern will propagate into any new code that touches these layers.

Suggested cleanup (could be a follow-up): Resolve to AuthConfig once at the boundary — in the OpenTDF / Client constructor. Internally, thread only auth: AuthConfig through all layers. The authProvider field stays on the public-facing OpenTDFOptions and ClientConfig for backwards compat, but gets resolved to AuthConfig immediately in the constructor and never passed further. The one exception is the legacy fetch fallback in access.ts that genuinely needs AuthProvider.withCreds() — but resolveAuthConfig() already handles extracting that for exactly this case.

This would shrink the internal API surface, eliminate the type assertions, and make it impossible to accidentally use the wrong auth field in new code.

marythought
marythought previously approved these changes Mar 31, 2026
Copy link
Copy Markdown
Contributor

@marythought marythought left a comment

Choose a reason for hiding this comment

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

see notes for some followup ideas -- a great improvement!

…internally

Remove dual auth/authProvider threading through internal layers.
AuthConfig is now resolved once in the Client constructor from either
authProvider or interceptors. Only `auth: AuthConfig` is passed
through EncryptConfiguration, DecryptConfiguration, and unwrapKey.

The authProvider field remains on the public-facing ClientConfig and
OpenTDFOptions for backwards compatibility but is never passed further
than the constructor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mark authProvider as deprecated since 0.14.0 across OpenTDFOptions,
PlatformClientOptions, and ClientConfig.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
lib/tests/web/opentdf.test.ts (1)

125-132: Test assertion is tangential to the test name.

The test is titled "does not call updateClientPublicKey with interceptors" but the assertion checks dpopEnabled. While the test logic is valid (no authProvider means no updateClientPublicKey to call), consider either:

  • Renaming to "interceptors-only mode keeps dpopEnabled true", or
  • Adding a spy/tracking mechanism similar to the trackingAuthProvider pattern used in lines 56-68 to explicitly verify the absence of a call.

The current test still provides value by confirming the interceptor path completes without error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/tests/web/opentdf.test.ts` around lines 125 - 132, The test name and
assertion mismatch: it instantiates OpenTDF with interceptors via
authTokenInterceptor and asserts client.dpopEnabled, but the title says "does
not call updateClientPublicKey with interceptors"; either rename the test to
reflect what is asserted (e.g., "interceptors-only mode keeps dpopEnabled true")
or add an explicit spy like the existing trackingAuthProvider pattern to verify
updateClientPublicKey was not called — locate the OpenTDF instantiation and
client.ready usage in this test and either change the it(...) description to
match the dpopEnabled assertion or inject a mock/tracker for
updateClientPublicKey and assert it was never invoked.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/tests/web/opentdf.test.ts`:
- Around line 125-132: The test name and assertion mismatch: it instantiates
OpenTDF with interceptors via authTokenInterceptor and asserts
client.dpopEnabled, but the title says "does not call updateClientPublicKey with
interceptors"; either rename the test to reflect what is asserted (e.g.,
"interceptors-only mode keeps dpopEnabled true") or add an explicit spy like the
existing trackingAuthProvider pattern to verify updateClientPublicKey was not
called — locate the OpenTDF instantiation and client.ready usage in this test
and either change the it(...) description to match the dpopEnabled assertion or
inject a mock/tracker for updateClientPublicKey and assert it was never invoked.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ddcabe31-94d9-4705-8444-ae8f263ca982

📥 Commits

Reviewing files that changed from the base of the PR and between 4337118 and 6fcf726.

📒 Files selected for processing (4)
  • lib/src/opentdf.ts
  • lib/tdf3/src/client/index.ts
  • lib/tdf3/src/tdf.ts
  • lib/tests/web/opentdf.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/tdf3/src/tdf.ts
  • lib/tdf3/src/client/index.ts

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 1, 2026

X-Test Failure Report

opentdf-ctl
opentdf-sdk-lib

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 1, 2026

marythought added a commit to opentdf/docs that referenced this pull request Apr 1, 2026
Updates all JavaScript/TypeScript code examples to use the new
Connect RPC interceptor-based authentication introduced in
opentdf/web-sdk#899, replacing the deprecated AuthProvider pattern.

Key changes:
- authTokenInterceptor as primary auth mechanism
- "define once, pass everywhere" pattern for shared auth config
- Remove await client.ready (no longer needed with interceptors)
- AuthProvider examples moved to legacy sections
- Standalone functions (listAttributes, etc.) use AuthConfig

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

@marythought marythought left a comment

Choose a reason for hiding this comment

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

🚀

@eugenioenko eugenioenko merged commit 1be800e into main Apr 1, 2026
23 checks passed
@eugenioenko eugenioenko deleted the feat/interceptor-auth-pattern branch April 1, 2026 16:47
marythought added a commit that referenced this pull request Apr 1, 2026
Adds convenience factories that return a TokenProvider for use with
authTokenInterceptor, mirroring the existing AuthProviders pattern:

- clientCredentialsTokenProvider: OAuth2 client credentials grant
- refreshTokenProvider: refresh token exchange
- externalJwtTokenProvider: RFC 8693 token exchange

All providers cache tokens and auto-refresh when the JWT exp claim
indicates expiration (with 30s buffer). This closes the ergonomics
gap between the deprecated AuthProvider pattern and the new
interceptor-based auth from #899.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
marythought added a commit that referenced this pull request Apr 2, 2026
Adds convenience factories that return a TokenProvider for use with
authTokenInterceptor, mirroring the existing AuthProviders pattern:

- clientCredentialsTokenProvider: OAuth2 client credentials grant
- refreshTokenProvider: refresh token exchange
- externalJwtTokenProvider: RFC 8693 token exchange

All providers cache tokens and auto-refresh when the JWT exp claim
indicates expiration (with 30s buffer). This closes the ergonomics
gap between the deprecated AuthProvider pattern and the new
interceptor-based auth from #899.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
marythought added a commit that referenced this pull request Apr 6, 2026
Adds convenience factories that return a TokenProvider for use with
authTokenInterceptor, mirroring the existing AuthProviders pattern:

- clientCredentialsTokenProvider: OAuth2 client credentials grant
- refreshTokenProvider: refresh token exchange
- externalJwtTokenProvider: RFC 8693 token exchange

All providers cache tokens and auto-refresh when the JWT exp claim
indicates expiration (with 30s buffer). This closes the ergonomics
gap between the deprecated AuthProvider pattern and the new
interceptor-based auth from #899.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Mary Dickson <mary.dickson@virtru.com>
marythought added a commit that referenced this pull request Apr 6, 2026
…906)

* feat(sdk): add TokenProvider factory functions for common OIDC flows

Adds convenience factories that return a TokenProvider for use with
authTokenInterceptor, mirroring the existing AuthProviders pattern:

- clientCredentialsTokenProvider: OAuth2 client credentials grant
- refreshTokenProvider: refresh token exchange
- externalJwtTokenProvider: RFC 8693 token exchange

All providers cache tokens and auto-refresh when the JWT exp claim
indicates expiration (with 30s buffer). This closes the ergonomics
gap between the deprecated AuthProvider pattern and the new
interceptor-based auth from #899.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Mary Dickson <mary.dickson@virtru.com>

* fix(sdk): address review feedback on token providers

- Add in-flight promise deduplication to all three providers to prevent
  concurrent token fetches and refresh token rotation races
- Support expires_in from token response for cache TTL (opaque tokens)
- Validate oidcOrigin is non-empty to prevent silent credential leaks
- Add regression tests for concurrency, opaque token caching, and
  blank oidcOrigin rejection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Mary Dickson <mary.dickson@virtru.com>

* chore(sdk): suppress no-unused-vars lint for mockFetch params

The parameters are needed for sinon's type inference (tests access
fetchFake.firstCall.args), so add an eslint-disable comment rather
than removing them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Mary Dickson <mary.dickson@virtru.com>

* chore(sdk): add error response handling tests for token providers

Each provider now has a test verifying that a failed token fetch
rejects the promise and clears the in-flight state, allowing a
subsequent retry to succeed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Mary Dickson <mary.dickson@virtru.com>

* 🤖 🎨 Autoformat

Signed-off-by: Mary Dickson <mary.dickson@virtru.com>

* chore(sdk): add server-side-only warning to clientCredentialsTokenProvider

Client secrets must not be exposed in browser code. Adds JSDoc warnings
to both the options type and the factory function per reviewer feedback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Mary Dickson <mary.dickson@virtru.com>

* chore(sdk): add buffer refresh, fallback, and exact assertion tests

- Test 30-second pre-expiry refresh buffer on clientCredentialsTokenProvider
- Test externalJwtTokenProvider re-exchange fallback when no refresh_token
- Use URLSearchParams for exact RFC 8693 form field assertions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Mary Dickson <mary.dickson@virtru.com>

* 🤖 🎨 Autoformat

Signed-off-by: Mary Dickson <mary.dickson@virtru.com>

---------

Signed-off-by: Mary Dickson <mary.dickson@virtru.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: marythought <10136229+marythought@users.noreply.github.com>
marythought added a commit to opentdf/docs that referenced this pull request Apr 7, 2026
## Summary

Updates all JavaScript/TypeScript code examples across SDK documentation
to use the new Connect RPC interceptor-based authentication pattern with
built-in token provider factories, replacing the deprecated
`AuthProvider` approach and manual token fetching.

**Dependencies:** opentdf/web-sdk#899 (interceptor pattern) and
opentdf/web-sdk#906 (token provider factories) — both merged to main.

## What changed

### Auth pattern migration
- Manual `getAccessToken()` implementations replaced with
`clientCredentialsTokenProvider()`, `refreshTokenProvider()`,
`externalJwtTokenProvider()` factory functions
- `AuthProviders.clientSecretAuthProvider()` replaced with
`authTokenInterceptor()` + token provider factories as the primary JS
auth pattern
- "Define once, pass everywhere" idiom: create an `auth` config object
once, reuse for `OpenTDF`, `PlatformClient`, and standalone functions
- Removed all `await client.ready` — no longer needed with interceptors
- `AuthProvider` examples preserved in Legacy sections for backwards
compatibility

### Browser-first messaging
- Added shared admonition (`code_samples/js_auth_note.mdx`) across all
SDK pages clarifying that `clientCredentialsTokenProvider` is for
learning/server-side only — the JS SDK is designed for browser apps
- Authentication page tip now leads with browser guidance, pointing to
`refreshTokenProvider()` as the primary JS path
- Client credentials warning strengthened: "Server-side only — not for
browsers"
- Quickstart frames Node.js as a convenience for learning, not the
intended runtime
- Certificate exchange (mTLS) section updated: browsers handle client
certs at the OS level, no SDK config needed

### Developer experience improvements
- Quickstart uses `attributeExists()` and `attributeValueExists()`
discovery helpers instead of manual list+filter patterns
- Subject condition set code collapsed from pyramid nesting to `[{`
style with explanatory comments
- `getMyToken()` in Custom Token Source section now has a concrete
definition with OIDC library examples
- DPoP section expanded with Go and Java tabs (was JS-only)
- CreateTDF and LoadTDF parameter tables split into per-SDK tabs
matching actual signatures
- Troubleshooting "Getting Help" links to all 3 SDK repos
- Platform client Go example shows service usage with authorization v2

### Files modified

**New:**
- `code_samples/js_auth_note.mdx` — shared browser-use admonition
partial

**Docs (9 files):**
- `docs/sdks/authentication.mdx` — all 4 auth patterns + DPoP tabs +
mTLS fix + legacy in tabs
- `docs/sdks/quickstart/javascript.mdx` — browser-first framing,
discovery helpers, cleaner code
- `docs/sdks/discovery.mdx` — shared admonition replaces old tip
- `docs/sdks/tdf.mdx` — per-SDK parameter tables, shared admonition
- `docs/sdks/authorization.mdx` — shared admonition
- `docs/sdks/platform-client.mdx` — shared admonition, Go service usage
example
- `docs/sdks/policy.mdx` — shared admonition
- `docs/sdks/troubleshooting.mdx` — shared admonition, SDK repo links
- `docs/guides/authentication-guide.mdx` — decision guide references to
factory functions

**Code samples (7 files):**
- All files in `code_samples/policy_code/` — inline interceptor +
`clientCredentialsTokenProvider` pattern

## Follow-up

Sidebar reordering, example placement, and JS/TS tab naming consistency
tracked in #274.

## Test plan

- [x] `npm run build` passes
- [x] Surge preview renders admonitions correctly across all pages
- [x] Code examples are copy-pasteable with correct imports
- [x] Legacy AuthProvider section still present and accurate

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Updated SDK guides and examples to use a unified interceptor-based
authentication approach
* Added JavaScript-specific auth callouts clarifying client-credentials
are server-only and browser apps should use OIDC/refresh-token flows
* Revised quickstarts, samples, and troubleshooting with explicit
token-provider guidance and token lifecycle notes
* Added legacy deprecation guidance while noting backward compatibility
remains intact
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Mary Dickson <mary.dickson@virtru.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

3 participants