From 90386d5fdd7a512c71a798aa20c5e322f48a8e9e Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 31 Mar 2026 18:41:45 -0700 Subject: [PATCH 01/12] feat(docs): update JS SDK examples to interceptor auth pattern 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) --- code_samples/policy_code/create_attribute.mdx | 2 +- code_samples/policy_code/create_namespace.mdx | 2 +- .../create_subject_condition_set.mdx | 2 +- .../policy_code/create_subject_mapping.mdx | 2 +- code_samples/policy_code/list_attributes.mdx | 2 +- code_samples/policy_code/list_namespaces.mdx | 2 +- .../policy_code/list_subject_mapping.mdx | 2 +- docs/sdks/authentication.mdx | 144 ++++++++++++------ docs/sdks/authorization.mdx | 26 ++-- docs/sdks/discovery.mdx | 45 +++--- docs/sdks/platform-client.mdx | 21 +-- docs/sdks/policy.mdx | 86 +++++------ docs/sdks/quickstart/javascript.mdx | 77 ++++++---- docs/sdks/tdf.mdx | 24 +-- docs/sdks/troubleshooting.mdx | 17 +-- specs/policy/selectors.openapi.yaml | 7 + 16 files changed, 260 insertions(+), 201 deletions(-) diff --git a/code_samples/policy_code/create_attribute.mdx b/code_samples/policy_code/create_attribute.mdx index 844b049b..314439bf 100644 --- a/code_samples/policy_code/create_attribute.mdx +++ b/code_samples/policy_code/create_attribute.mdx @@ -77,7 +77,7 @@ import { PlatformClient } from '@opentdf/sdk/platform'; import { AttributeRuleTypeEnum } from '@opentdf/sdk/platform/policy/objects_pb.js'; const platform = new PlatformClient({ - authProvider, + ...auth, platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/create_namespace.mdx b/code_samples/policy_code/create_namespace.mdx index 954b9345..2d1bded1 100644 --- a/code_samples/policy_code/create_namespace.mdx +++ b/code_samples/policy_code/create_namespace.mdx @@ -57,7 +57,7 @@ import CreateNamespaceExample from '@site/code_samples/java/create-namespace.mdx import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - authProvider, + ...auth, platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/create_subject_condition_set.mdx b/code_samples/policy_code/create_subject_condition_set.mdx index e560520a..1e17fe35 100644 --- a/code_samples/policy_code/create_subject_condition_set.mdx +++ b/code_samples/policy_code/create_subject_condition_set.mdx @@ -86,7 +86,7 @@ import { } from '@opentdf/sdk/platform/policy/subjectmapping/subject_mapping_pb.js'; const platform = new PlatformClient({ - authProvider, + ...auth, platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/create_subject_mapping.mdx b/code_samples/policy_code/create_subject_mapping.mdx index 17e81459..0276955f 100644 --- a/code_samples/policy_code/create_subject_mapping.mdx +++ b/code_samples/policy_code/create_subject_mapping.mdx @@ -66,7 +66,7 @@ import CreateSubjectMappingExample from '@site/code_samples/java/create-subject- import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - authProvider, + ...auth, platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/list_attributes.mdx b/code_samples/policy_code/list_attributes.mdx index a224bb43..50088fa0 100644 --- a/code_samples/policy_code/list_attributes.mdx +++ b/code_samples/policy_code/list_attributes.mdx @@ -61,7 +61,7 @@ import ListAttributesExample from '@site/code_samples/java/list-attributes.mdx'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - authProvider, + ...auth, platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/list_namespaces.mdx b/code_samples/policy_code/list_namespaces.mdx index f5dc0bb1..76c24f83 100644 --- a/code_samples/policy_code/list_namespaces.mdx +++ b/code_samples/policy_code/list_namespaces.mdx @@ -56,7 +56,7 @@ import ListNamespacesExample from '@site/code_samples/java/list-namespaces.mdx'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - authProvider, + ...auth, platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/list_subject_mapping.mdx b/code_samples/policy_code/list_subject_mapping.mdx index a0ea3326..15dd0913 100644 --- a/code_samples/policy_code/list_subject_mapping.mdx +++ b/code_samples/policy_code/list_subject_mapping.mdx @@ -59,7 +59,7 @@ import ListSubjectMappingsExample from '@site/code_samples/java/list-subject-map import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - authProvider, + ...auth, platformUrl: 'http://localhost:8080', }); diff --git a/docs/sdks/authentication.mdx b/docs/sdks/authentication.mdx index 91cf9da4..e4340a38 100644 --- a/docs/sdks/authentication.mdx +++ b/docs/sdks/authentication.mdx @@ -48,30 +48,35 @@ SDK sdk = new SDKBuilder() ```typescript -import { AuthProviders, OpenTDF } from '@opentdf/sdk'; - -const authProvider = await AuthProviders.clientSecretAuthProvider({ - clientId: 'my-client-id', - clientSecret: 'my-client-secret', - oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', - exchange: 'client', -}); +import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; + +// Use your preferred OAuth2 library to obtain tokens via client credentials grant. +// The interceptor calls your token provider per-request. +async function getAccessToken(): Promise { + // Example: fetch token from your IdP's token endpoint + const resp = await fetch('http://localhost:8080/auth/realms/opentdf/protocol/openid-connect/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'grant_type=client_credentials&client_id=my-client-id&client_secret=my-client-secret', + }); + const { access_token } = await resp.json(); + return access_token; +} const client = new OpenTDF({ - authProvider, + interceptors: [authTokenInterceptor(getAccessToken)], platformUrl: 'http://localhost:8080', }); -await client.ready; ``` :::warning Confidential clients only -`clientSecretAuthProvider` should be used only in Node.js or server-side environments. Never expose client secrets in browser code. +Client credentials should be used only in Node.js or server-side environments. Never expose client secrets in browser code. ::: -**Token refresh:** The SDK automatically obtains a new token when the current one expires. No manual refresh handling is needed. +**Token lifecycle:** The SDK calls your token provider on each request. Your provider is responsible for caching and refreshing tokens as needed. ## Token Exchange @@ -110,20 +115,29 @@ The Java SDK wraps the JWT as a `BearerAccessToken` and performs an RFC 8693 tok ```typescript -import { AuthProviders, OpenTDF } from '@opentdf/sdk'; - -const authProvider = await AuthProviders.externalAuthProvider({ - clientId: 'my-client-id', - externalJwt: 'eyJhbGciOi...', - oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', - exchange: 'external', -}); +import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; + +// Exchange your external JWT for a platform token, then provide it to the SDK. +async function getExchangedToken(): Promise { + // Perform RFC 8693 token exchange with your IdP + const resp = await fetch('http://localhost:8080/auth/realms/opentdf/protocol/openid-connect/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + subject_token: 'eyJhbGciOi...', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + client_id: 'my-client-id', + }), + }); + const { access_token } = await resp.json(); + return access_token; +} const client = new OpenTDF({ - authProvider, + interceptors: [authTokenInterceptor(getExchangedToken)], platformUrl: 'http://localhost:8080', }); -await client.ready; ``` @@ -138,20 +152,15 @@ Refresh token authentication is currently available in the JavaScript SDK only. ::: ```typescript -import { AuthProviders, OpenTDF } from '@opentdf/sdk'; - -const authProvider = await AuthProviders.refreshAuthProvider({ - clientId: 'my-client-id', - refreshToken: 'refresh-token-from-login-flow', - oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', - exchange: 'refresh', -}); +import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; +// For browser apps: use your OIDC library's token refresh mechanism. +// The interceptor calls your provider per-request — it should return a valid token, +// refreshing automatically when needed. const client = new OpenTDF({ - authProvider, + interceptors: [authTokenInterceptor(() => myOidcClient.getAccessToken())], platformUrl: 'http://localhost:8080', }); -await client.ready; ``` ## Certificate Exchange (mTLS) @@ -264,27 +273,19 @@ SDK sdk = new SDKBuilder() -Implement the `AuthProvider` interface: +Write a custom interceptor for full control over request authentication: ```typescript -import type { AuthProvider } from '@opentdf/sdk'; - -const customProvider: AuthProvider = { - async updateClientPublicKey(signingKey) { - // Store the signing key for DPoP, if applicable - }, - async withCreds(httpReq) { - // Add authentication headers to the request - httpReq.headers = { - ...httpReq.headers, - Authorization: `Bearer ${await getMyToken()}`, - }; - return httpReq; - }, +import { type Interceptor } from '@connectrpc/connect'; +import { OpenTDF } from '@opentdf/sdk'; + +const myAuthInterceptor: Interceptor = (next) => async (req) => { + req.header.set('Authorization', `Bearer ${await getMyToken()}`); + return next(req); }; const client = new OpenTDF({ - authProvider: customProvider, + interceptors: [myAuthInterceptor], platformUrl: 'http://localhost:8080', }); ``` @@ -300,8 +301,53 @@ const client = new OpenTDF({ |-----|-----------------|---------------| | **Go** | DPoP key auto-generated | `sdk.WithSessionSignerRSA(key)` to provide your own RSA key | | **Java** | Always on (RSA, auto-generated) | `SDKBuilder.srtSigner(signer)` for custom signing | -| **JavaScript** | On by default | `disableDPoP: true` to opt out, `dpopKeys` for custom keys | +| **JavaScript** | Off by default with interceptors | Use `authTokenDPoPInterceptor()` to enable | + + + + +```typescript +import { authTokenDPoPInterceptor, OpenTDF } from '@opentdf/sdk'; + +const dpopInterceptor = authTokenDPoPInterceptor({ + tokenProvider: () => getAccessToken(), +}); + +const client = new OpenTDF({ + interceptors: [dpopInterceptor], + dpopKeys: dpopInterceptor.dpopKeys, + platformUrl: 'http://localhost:8080', +}); +``` + + + :::warning Only disable DPoP if your IdP does not support it. DPoP is recommended for production deployments as it provides protection against token exfiltration. ::: + +## Legacy: AuthProvider + +:::info +The `AuthProvider` interface is deprecated as of the interceptor-based auth introduced in `@opentdf/sdk`. Existing code using `AuthProvider` continues to work. For new code, use interceptors as shown above. +::: + +The legacy `AuthProvider` pattern managed token lifecycle internally: + +```typescript +import { AuthProviders, OpenTDF } from '@opentdf/sdk'; + +const authProvider = await AuthProviders.clientSecretAuthProvider({ + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + exchange: 'client', +}); + +const client = new OpenTDF({ + authProvider, + platformUrl: 'http://localhost:8080', +}); +await client.ready; +``` diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index a59e6848..548351b0 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -94,10 +94,18 @@ public class AuthorizationSetup { ```typescript +// Option 1: Using interceptors (recommended for new code) +import { authTokenInterceptor } from '@opentdf/sdk'; +import { PlatformClient } from '@opentdf/sdk/platform'; + +const platformClient = new PlatformClient({ + interceptors: [authTokenInterceptor(getAccessToken)], + platformUrl: 'http://localhost:8080', +}); + +// Option 2: Legacy AuthProvider (deprecated — use interceptors for new code) import { AuthProviders, OpenTDF } from '@opentdf/sdk'; -import { platformConnect, PlatformClient } from '@opentdf/sdk/platform'; -// Option 1: Using AuthProvider (recommended when also using TDF client) const authProvider = await AuthProviders.clientSecretAuthProvider({ clientId: 'opentdf', clientSecret: 'secret', @@ -106,21 +114,9 @@ const authProvider = await AuthProviders.clientSecretAuthProvider({ const client = new OpenTDF({ authProvider, platformUrl: 'http://localhost:8080' }); await client.ready; -const platformClient = new PlatformClient({ - authProvider, - platformUrl: 'http://localhost:8080', -}); - -// Option 2: Using a raw interceptor (when you manage tokens yourself) -const accessToken = 'your-access-token-here'; -const interceptor: platformConnect.Interceptor = (next) => async (req) => { - req.header.set('Authorization', `Bearer ${accessToken}`); - return next(req); -}; - const platformClient2 = new PlatformClient({ + authProvider, platformUrl: 'http://localhost:8080', - interceptors: [interceptor], }); ``` diff --git a/docs/sdks/discovery.mdx b/docs/sdks/discovery.mdx index d7280667..8c046ce7 100644 --- a/docs/sdks/discovery.mdx +++ b/docs/sdks/discovery.mdx @@ -65,9 +65,11 @@ List attrs = sdk.listAttributes("opentdf.io"); ```ts -import { listAttributes } from '@opentdf/sdk'; +import { authTokenInterceptor, listAttributes } from '@opentdf/sdk'; -const attrs = await listAttributes(platformUrl, authProvider); +const auth = { interceptors: [authTokenInterceptor(getAccessToken)] }; + +const attrs = await listAttributes(platformUrl, auth); for (const a of attrs) { console.log(a.fqn, a.rule); @@ -80,7 +82,7 @@ for (const a of attrs) { To filter by namespace: ```ts -const attrs = await listAttributes(platformUrl, authProvider, 'opentdf.io'); +const attrs = await listAttributes(platformUrl, auth, 'opentdf.io'); ``` @@ -127,9 +129,11 @@ if (!exists) { ```ts -import { attributeExists } from '@opentdf/sdk'; +import { authTokenInterceptor, attributeExists } from '@opentdf/sdk'; + +const auth = { interceptors: [authTokenInterceptor(getAccessToken)] }; -const exists = await attributeExists(platformUrl, authProvider, 'https://opentdf.io/attr/department'); +const exists = await attributeExists(platformUrl, auth, 'https://opentdf.io/attr/department'); if (!exists) { console.log('attribute does not exist — create it before use'); } @@ -177,9 +181,11 @@ if (!exists) { ```ts -import { attributeValueExists } from '@opentdf/sdk'; +import { authTokenInterceptor, attributeValueExists } from '@opentdf/sdk'; + +const auth = { interceptors: [authTokenInterceptor(getAccessToken)] }; -const exists = await attributeValueExists(platformUrl, authProvider, 'https://opentdf.io/attr/department/value/finance'); +const exists = await attributeValueExists(platformUrl, auth, 'https://opentdf.io/attr/department/value/finance'); if (!exists) { console.log('value does not exist on this attribute'); } @@ -245,7 +251,9 @@ sdk.createTDF(inputStream, outputStream, config); ```ts -import { validateAttributes, AttributeNotFoundError } from '@opentdf/sdk'; +import { authTokenInterceptor, validateAttributes, AttributeNotFoundError } from '@opentdf/sdk'; + +const auth = { interceptors: [authTokenInterceptor(getAccessToken)] }; const fqns = [ 'https://opentdf.io/attr/department/value/marketing', @@ -253,7 +261,7 @@ const fqns = [ ]; try { - await validateAttributes(platformUrl, authProvider, fqns); + await validateAttributes(platformUrl, auth, fqns); } catch (e) { if (e instanceof AttributeNotFoundError) { console.error('attribute validation failed:', e.message); @@ -356,7 +364,7 @@ The JavaScript SDK does not expose a `getEntityAttributes` convenience function ```ts import { PlatformClient } from '@opentdf/sdk/platform'; -const platform = new PlatformClient({ authProvider, platformUrl }); +const platform = new PlatformClient({ ...auth, platformUrl }); // By email address const resp = await platform.v2.authorization.getEntitlements({ @@ -495,20 +503,17 @@ try (SDK sdk = new SDKBuilder() ```ts -import { OpenTDF, AuthProviders, listAttributes, attributeExists, validateAttributes, AttributeNotFoundError } from '@opentdf/sdk'; +import { authTokenInterceptor, OpenTDF, listAttributes, attributeExists, validateAttributes, AttributeNotFoundError } from '@opentdf/sdk'; -const authProvider = await AuthProviders.clientSecretAuthProvider({ - clientId: 'opentdf', - clientSecret: 'secret', - oidcOrigin: oidcEndpoint, -}); +// Define auth once — reuse for standalone functions and the OpenTDF client +const auth = { interceptors: [authTokenInterceptor(getAccessToken)] }; // 1. See what's available on the platform -const attrs = await listAttributes(platformUrl, authProvider); +const attrs = await listAttributes(platformUrl, auth); console.log(`platform has ${attrs.length} active attributes`); // 2. Check a specific attribute exists before using it -const exists = await attributeExists(platformUrl, authProvider, 'https://opentdf.io/attr/department'); +const exists = await attributeExists(platformUrl, auth, 'https://opentdf.io/attr/department'); if (!exists) { console.error('attribute missing — create it first'); process.exit(1); @@ -517,7 +522,7 @@ if (!exists) { // 3. Validate the specific values before encrypting const required = ['https://opentdf.io/attr/department/value/marketing']; try { - await validateAttributes(platformUrl, authProvider, required); + await validateAttributes(platformUrl, auth, required); } catch (e) { if (e instanceof AttributeNotFoundError) { console.error('required attributes missing — create them first:', e.message); @@ -527,7 +532,7 @@ try { } // 4. Encrypt with confidence -const client = new OpenTDF({ authProvider, platformUrl }); +const client = new OpenTDF({ ...auth, platformUrl }); const ciphertext = await client.createTDF({ source: { type: 'stream', location: dataStream }, attributes: required, diff --git a/docs/sdks/platform-client.mdx b/docs/sdks/platform-client.mdx index c902101a..e498a0ce 100644 --- a/docs/sdks/platform-client.mdx +++ b/docs/sdks/platform-client.mdx @@ -62,24 +62,13 @@ SDK sdk = SDKBuilder.newBuilder() ```typescript -import { AuthProviders, OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -const authProvider = await AuthProviders.clientSecretAuthProvider({ - clientId: 'client-id', - clientSecret: 'client-secret', - oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', -}); - -// OpenTDF eagerly binds DPoP keys to the auth provider. -// Await ready before creating PlatformClient. -const client = new OpenTDF({ authProvider, platformUrl: 'http://localhost:8080' }); -await client.ready; - -const platform = new PlatformClient({ - authProvider, - platformUrl: 'http://localhost:8080', -}); +const interceptors = [authTokenInterceptor(getAccessToken)]; + +const client = new OpenTDF({ interceptors, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ interceptors, platformUrl: 'http://localhost:8080' }); ``` diff --git a/docs/sdks/policy.mdx b/docs/sdks/policy.mdx index d65c2ec8..0662fabd 100644 --- a/docs/sdks/policy.mdx +++ b/docs/sdks/policy.mdx @@ -127,9 +127,9 @@ public class GetNamespaceExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); // Look up by UUID let resp = await platform.v1.namespace.getNamespace({ id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479' }); @@ -235,9 +235,9 @@ public class UpdateNamespaceExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.namespace.updateNamespace({ id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', @@ -323,9 +323,9 @@ public class DeactivateNamespaceExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); await platform.v1.namespace.deactivateNamespace({ id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479' }); console.log('Namespace deactivated.'); @@ -436,9 +436,9 @@ public class GetAttributeExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); // Look up by FQN const resp = await platform.v1.attributes.getAttribute({ @@ -537,9 +537,9 @@ public class UpdateAttributeExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.attributes.updateAttribute({ id: 'a1b2c3d4-0000-0000-0000-000000000001', @@ -625,9 +625,9 @@ public class DeactivateAttributeExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.attributes.deactivateAttribute({ id: 'a1b2c3d4-0000-0000-0000-000000000001', @@ -717,9 +717,9 @@ public class CreateAttributeValueExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.attributes.createAttributeValue({ attributeId: 'a1b2c3d4-0000-0000-0000-000000000001', @@ -808,9 +808,9 @@ public class ListAttributeValuesExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.attributes.listAttributeValues({ attributeId: 'a1b2c3d4-0000-0000-0000-000000000001', @@ -899,9 +899,9 @@ public class GetAttributeValueExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); // Look up by FQN const resp = await platform.v1.attributes.getAttributeValue({ @@ -1003,9 +1003,9 @@ public class GetAttributeValuesByFqnsExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.attributes.getAttributeValuesByFqns({ fqns: [ @@ -1105,9 +1105,9 @@ public class UpdateAttributeValueExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.attributes.updateAttributeValue({ id: 'v1b2c3d4-0000-0000-0000-000000000001', @@ -1193,9 +1193,9 @@ public class DeactivateAttributeValueExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.attributes.deactivateAttributeValue({ id: 'v1b2c3d4-0000-0000-0000-000000000001', @@ -1290,9 +1290,9 @@ public class ListSubjectConditionSetsExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.subjectMapping.listSubjectConditionSets({}); for (const scs of resp.subjectConditionSets) { @@ -1381,9 +1381,9 @@ public class GetSubjectConditionSetExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.subjectMapping.getSubjectConditionSet({ id: 'a0b1c2d3-0000-0000-0000-000000000099', @@ -1505,9 +1505,9 @@ public class UpdateSubjectConditionSetExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.subjectMapping.updateSubjectConditionSet({ id: 'a0b1c2d3-0000-0000-0000-000000000099', @@ -1607,9 +1607,9 @@ public class DeleteSubjectConditionSetExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.subjectMapping.deleteSubjectConditionSet({ id: 'a0b1c2d3-0000-0000-0000-000000000099', @@ -1690,9 +1690,9 @@ public class DeleteAllUnmappedSubjectConditionSetsExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.subjectMapping.deleteAllUnmappedSubjectConditionSets({}); console.log(`Deleted ${resp.subjectConditionSets.length} unmapped subject condition sets.`); @@ -1787,9 +1787,9 @@ public class GetSubjectMappingExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.subjectMapping.getSubjectMapping({ id: '890b26db-4ee4-447f-ae8a-2862d922eeef', @@ -1892,9 +1892,9 @@ public class UpdateSubjectMappingExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.subjectMapping.updateSubjectMapping({ id: '890b26db-4ee4-447f-ae8a-2862d922eeef', @@ -1979,9 +1979,9 @@ public class DeleteSubjectMappingExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.subjectMapping.deleteSubjectMapping({ id: '890b26db-4ee4-447f-ae8a-2862d922eeef', @@ -2083,9 +2083,9 @@ public class MatchSubjectMappingsExample { ```typescript import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including DPoP key binding. +// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ authProvider, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); const resp = await platform.v1.subjectMapping.matchSubjectMappings({ subjectProperties: [ @@ -2131,7 +2131,7 @@ if err != nil { import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - authProvider, + ...auth, platformUrl: 'http://localhost:8080', }); diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index 7763ecde..7ec4581d 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -70,7 +70,7 @@ Expected output: Create a file named `index.mjs`: ```javascript title="index.mjs" -import { AuthProviders, OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; async function main() { console.log('🚀 Starting OpenTDF SDK Quickstart...'); @@ -79,16 +79,20 @@ async function main() { const oidcOrigin = 'https://keycloak.opentdf.local:9443/auth/realms/opentdf'; console.log(`📡 Connecting to platform: ${platformUrl}`); - // Create an auth provider with client credentials - console.log('🔐 Initializing auth provider with client credentials...'); - const authProvider = await AuthProviders.clientSecretAuthProvider({ - clientId: 'opentdf', - clientSecret: 'secret', - oidcOrigin, - }); + // Token provider function for client credentials + async function getAccessToken() { + const resp = await fetch(`${oidcOrigin}/protocol/openid-connect/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'grant_type=client_credentials&client_id=opentdf&client_secret=secret', + }); + const { access_token } = await resp.json(); + return access_token; + } // Create a new OpenTDF client - const client = new OpenTDF({ authProvider, platformUrl }); + console.log('🔐 Initializing client with auth interceptor...'); + const client = new OpenTDF({ interceptors: [authTokenInterceptor(getAccessToken)], platformUrl }); console.log('✅ SDK client initialized successfully'); // Encrypt data @@ -140,7 +144,7 @@ NODE_TLS_REJECT_UNAUTHORIZED=0 node index.mjs ```console 🚀 Starting OpenTDF SDK Quickstart... 📡 Connecting to platform: https://platform.opentdf.local:8443 -🔐 Initializing auth provider with client credentials... +🔐 Initializing client with auth interceptor... ✅ SDK client initialized successfully 📝 Encrypting sensitive data... @@ -185,21 +189,28 @@ For additional policy management examples including managing attributes, namespa Let's work with the "department" attribute and add a "marketing" value. If you completed the [Quickstart setup guide](/quickstart), the attribute may already exist with finance and engineering values. If not, we'll create it: ```javascript -import { AuthProviders, OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; import { AttributeRuleTypeEnum } from '@opentdf/sdk/platform/policy/objects_pb.js'; const platformUrl = 'https://platform.opentdf.local:8443'; -const authProvider = await AuthProviders.clientSecretAuthProvider({ - clientId: 'opentdf', - clientSecret: 'secret', - oidcOrigin: 'https://keycloak.opentdf.local:9443/auth/realms/opentdf', -}); +const oidcOrigin = 'https://keycloak.opentdf.local:9443/auth/realms/opentdf'; + +// Token provider function for client credentials +async function getAccessToken() { + const resp = await fetch(`${oidcOrigin}/protocol/openid-connect/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'grant_type=client_credentials&client_id=opentdf&client_secret=secret', + }); + const { access_token } = await resp.json(); + return access_token; +} -// Create the OpenTDF client and wait for DPoP key binding. -const client = new OpenTDF({ authProvider, platformUrl }); -await client.ready; -const platform = new PlatformClient({ authProvider, platformUrl }); +// Create the OpenTDF and Platform clients with auth interceptor +const interceptors = [authTokenInterceptor(getAccessToken)]; +const client = new OpenTDF({ interceptors, platformUrl }); +const platform = new PlatformClient({ interceptors, platformUrl }); // First, ensure the namespace exists let namespaceId; @@ -396,7 +407,7 @@ For reference, here's a complete example showing all the pieces together: ```javascript title="index.mjs" import fs from 'node:fs'; -import { AuthProviders, OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; import { AttributeRuleTypeEnum, @@ -408,17 +419,21 @@ async function main() { const platformUrl = 'https://platform.opentdf.local:8443'; const oidcOrigin = 'https://keycloak.opentdf.local:9443/auth/realms/opentdf'; - // Create auth provider - const authProvider = await AuthProviders.clientSecretAuthProvider({ - clientId: 'opentdf', - clientSecret: 'secret', - oidcOrigin, - }); + // Token provider function for client credentials + async function getAccessToken() { + const resp = await fetch(`${oidcOrigin}/protocol/openid-connect/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'grant_type=client_credentials&client_id=opentdf&client_secret=secret', + }); + const { access_token } = await resp.json(); + return access_token; + } - // Create the OpenTDF client and wait for DPoP key binding. - const client = new OpenTDF({ authProvider, platformUrl }); - await client.ready; - const platform = new PlatformClient({ authProvider, platformUrl }); + // Create the OpenTDF and Platform clients with auth interceptor + const interceptors = [authTokenInterceptor(getAccessToken)]; + const client = new OpenTDF({ interceptors, platformUrl }); + const platform = new PlatformClient({ interceptors, platformUrl }); // 1. Create namespace (or use existing) let namespaceId; diff --git a/docs/sdks/tdf.mdx b/docs/sdks/tdf.mdx index f746d9f5..6b900710 100644 --- a/docs/sdks/tdf.mdx +++ b/docs/sdks/tdf.mdx @@ -118,15 +118,19 @@ try (FileChannel ch = FileChannel.open(tmp, StandardOpenOption.READ)) { ```typescript -import { OpenTDF, AuthProviders } from '@opentdf/sdk'; - -const authProvider = await AuthProviders.clientSecretAuthProvider({ - clientId: 'client-id', - clientSecret: 'client-secret', - oidcOrigin: 'https://platform.example.com/auth/realms/opentdf', -}); +import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; + +async function getAccessToken(): Promise { + const resp = await fetch('https://platform.example.com/auth/realms/opentdf/protocol/openid-connect/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'grant_type=client_credentials&client_id=client-id&client_secret=client-secret', + }); + const { access_token } = await resp.json(); + return access_token; +} -const client = new OpenTDF({ authProvider, platformUrl: 'https://platform.example.com' }); +const client = new OpenTDF({ interceptors: [authTokenInterceptor(getAccessToken)], platformUrl: 'https://platform.example.com' }); // Encrypt const enc = new TextEncoder(); @@ -942,9 +946,9 @@ for _, fqn := range obligations.FQNs { ```typescript -import { OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; -const client = new OpenTDF({ authProvider, platformUrl: 'http://localhost:8080' }); +const client = new OpenTDF({ interceptors: [authTokenInterceptor(getAccessToken)], platformUrl: 'http://localhost:8080' }); const reader = client.open({ source: ciphertextSource }); const obligations = await reader.obligations(); diff --git a/docs/sdks/troubleshooting.mdx b/docs/sdks/troubleshooting.mdx index e4ad2d96..2249472f 100644 --- a/docs/sdks/troubleshooting.mdx +++ b/docs/sdks/troubleshooting.mdx @@ -41,16 +41,13 @@ curl https:/// otdfctl auth client-credentials ``` -2. **Implement token refresh in your application**: The JavaScript SDK's `refreshAuthProvider` handles automatic token renewal: +2. **Implement token refresh in your application**: Use `authTokenInterceptor` with a function that returns a fresh token on each call. This ensures the SDK always has a valid token: ```js - import { AuthProviders } from '@opentdf/sdk'; - - const authProvider = AuthProviders.refreshAuthProvider({ - clientId: 'your-client-id', - exchange: 'client', - clientSecret: 'your-client-secret', - oidcOrigin: 'https://', // Keycloak: append /realms/; adjust path for your IdP - }); + import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; + + // getAccessToken should fetch/refresh a token from your IdP + const auth = { interceptors: [authTokenInterceptor(getAccessToken)] }; + const client = new OpenTDF({ ...auth, platformUrl }); ``` 3. **Re-instantiate the SDK**: For Go and Java, create a new SDK instance with fresh credentials when a token error occurs. @@ -320,7 +317,7 @@ exit status 1 - **Go**: `client.ValidateAttributes(ctx, fqns)` - **Java**: `sdk.validateAttributes(fqns)` -- **JavaScript**: `await validateAttributes(platformUrl, authProvider, fqns)` +- **JavaScript**: `await validateAttributes(platformUrl, auth, fqns)` **Solution**: Create the resource first using otdfctl or the SDK policy functions. For attributes, see the [SDK Create Attribute function](/sdks/policy#create-attribute). diff --git a/specs/policy/selectors.openapi.yaml b/specs/policy/selectors.openapi.yaml index e2d3c3a7..4afe988b 100644 --- a/specs/policy/selectors.openapi.yaml +++ b/specs/policy/selectors.openapi.yaml @@ -4,6 +4,13 @@ info: paths: {} components: schemas: + policy.SortDirection: + type: string + title: SortDirection + enum: + - SORT_DIRECTION_UNSPECIFIED + - SORT_DIRECTION_ASC + - SORT_DIRECTION_DESC policy.AttributeDefinitionSelector: type: object properties: From d61b169c3342741487aec04c0d2a2431822e0c86 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 1 Apr 2026 09:52:10 -0700 Subject: [PATCH 02/12] chore(deps): update vendored OpenAPI specs Co-Authored-By: Claude Opus 4.6 (1M context) --- .../policy/namespaces/namespaces.openapi.yaml | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/specs/policy/namespaces/namespaces.openapi.yaml b/specs/policy/namespaces/namespaces.openapi.yaml index 51632ed0..4a5b7e35 100644 --- a/specs/policy/namespaces/namespaces.openapi.yaml +++ b/specs/policy/namespaces/namespaces.openapi.yaml @@ -364,6 +364,13 @@ components: - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + policy.SortDirection: + type: string + title: SortDirection + enum: + - SORT_DIRECTION_UNSPECIFIED + - SORT_DIRECTION_ASC + - SORT_DIRECTION_DESC policy.SourceType: type: string title: SourceType @@ -375,6 +382,15 @@ components: Describes whether this kas is managed by the organization or if they imported the kas information from an external party. These two modes are necessary in order to encrypt a tdf dek with an external parties kas public key. + policy.namespaces.SortNamespacesType: + type: string + title: SortNamespacesType + enum: + - SORT_NAMESPACES_TYPE_UNSPECIFIED + - SORT_NAMESPACES_TYPE_NAME + - SORT_NAMESPACES_TYPE_FQN + - SORT_NAMESPACES_TYPE_CREATED_AT + - SORT_NAMESPACES_TYPE_UPDATED_AT common.Metadata: type: object properties: @@ -901,6 +917,13 @@ components: title: pagination description: Optional $ref: '#/components/schemas/policy.PageRequest' + sort: + type: array + items: + $ref: '#/components/schemas/policy.namespaces.NamespacesSort' + title: sort + maxItems: 1 + description: Optional title: ListNamespacesRequest additionalProperties: false policy.namespaces.ListNamespacesResponse: @@ -950,6 +973,17 @@ components: title: NamespaceKeyAccessServer additionalProperties: false description: Deprecated + policy.namespaces.NamespacesSort: + type: object + properties: + field: + title: field + $ref: '#/components/schemas/policy.namespaces.SortNamespacesType' + direction: + title: direction + $ref: '#/components/schemas/policy.SortDirection' + title: NamespacesSort + additionalProperties: false policy.namespaces.RemoveKeyAccessServerFromNamespaceRequest: type: object properties: From f798afaa908a23ba5418548130451169d546f618 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 1 Apr 2026 09:57:53 -0700 Subject: [PATCH 03/12] fix(docs): address review feedback on interceptor examples - Use URLSearchParams instead of raw string body in all getAccessToken - Add caching note to token provider functions - Use ...auth spread pattern in platform-client.mdx - Add getAccessToken definition to platform-client.mdx - Add setup tip to discovery.mdx and comment to authorization.mdx explaining getAccessToken comes from /sdks/authentication Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/authentication.mdx | 8 +++++++- docs/sdks/authorization.mdx | 1 + docs/sdks/discovery.mdx | 4 ++++ docs/sdks/platform-client.mdx | 21 ++++++++++++++++++--- docs/sdks/quickstart/javascript.mdx | 21 ++++++++++++++++++--- docs/sdks/tdf.mdx | 7 ++++++- 6 files changed, 54 insertions(+), 8 deletions(-) diff --git a/docs/sdks/authentication.mdx b/docs/sdks/authentication.mdx index e4340a38..844bfb6b 100644 --- a/docs/sdks/authentication.mdx +++ b/docs/sdks/authentication.mdx @@ -53,11 +53,16 @@ import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; // Use your preferred OAuth2 library to obtain tokens via client credentials grant. // The interceptor calls your token provider per-request. async function getAccessToken(): Promise { + // In production, cache tokens and refresh only when expired. // Example: fetch token from your IdP's token endpoint const resp = await fetch('http://localhost:8080/auth/realms/opentdf/protocol/openid-connect/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: 'grant_type=client_credentials&client_id=my-client-id&client_secret=my-client-secret', + body: new URLSearchParams({ + grant_type: 'client_credentials', + client_id: 'my-client-id', + client_secret: 'my-client-secret', + }), }); const { access_token } = await resp.json(); return access_token; @@ -119,6 +124,7 @@ import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; // Exchange your external JWT for a platform token, then provide it to the SDK. async function getExchangedToken(): Promise { + // In production, cache tokens and refresh only when expired. // Perform RFC 8693 token exchange with your IdP const resp = await fetch('http://localhost:8080/auth/realms/opentdf/protocol/openid-connect/token', { method: 'POST', diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index 548351b0..30af1ec3 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -95,6 +95,7 @@ public class AuthorizationSetup { ```typescript // Option 1: Using interceptors (recommended for new code) +// getAccessToken() returns a valid access token — see /sdks/authentication import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; diff --git a/docs/sdks/discovery.mdx b/docs/sdks/discovery.mdx index 8c046ce7..42baaa88 100644 --- a/docs/sdks/discovery.mdx +++ b/docs/sdks/discovery.mdx @@ -12,6 +12,10 @@ Before encrypting data with `CreateTDF`, it helps to verify that the attributes The Go, Java, and JavaScript SDKs provide five methods to discover and validate attributes without manual platform queries. +:::tip JavaScript setup +The JS examples below assume you have a `getAccessToken()` function that returns a valid access token. See [Authentication](/sdks/authentication) for how to set this up. The `auth` object can be shared across all SDK calls — define it once, pass it everywhere. +::: + --- ## ListAttributes diff --git a/docs/sdks/platform-client.mdx b/docs/sdks/platform-client.mdx index e498a0ce..2d5f0ba7 100644 --- a/docs/sdks/platform-client.mdx +++ b/docs/sdks/platform-client.mdx @@ -65,10 +65,25 @@ SDK sdk = SDKBuilder.newBuilder() import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -const interceptors = [authTokenInterceptor(getAccessToken)]; +async function getAccessToken(): Promise { + // In production, cache tokens and refresh only when expired. + const resp = await fetch('http://localhost:8080/auth/realms/opentdf/protocol/openid-connect/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'client_credentials', + client_id: 'client-id', + client_secret: 'client-secret', + }), + }); + const { access_token } = await resp.json(); + return access_token; +} + +const auth = { interceptors: [authTokenInterceptor(getAccessToken)] }; -const client = new OpenTDF({ interceptors, platformUrl: 'http://localhost:8080' }); -const platform = new PlatformClient({ interceptors, platformUrl: 'http://localhost:8080' }); +const client = new OpenTDF({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); ``` diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index 7ec4581d..8b7a663d 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -81,10 +81,15 @@ async function main() { // Token provider function for client credentials async function getAccessToken() { + // In production, cache tokens and refresh only when expired. const resp = await fetch(`${oidcOrigin}/protocol/openid-connect/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: 'grant_type=client_credentials&client_id=opentdf&client_secret=secret', + body: new URLSearchParams({ + grant_type: 'client_credentials', + client_id: 'opentdf', + client_secret: 'secret', + }), }); const { access_token } = await resp.json(); return access_token; @@ -198,10 +203,15 @@ const oidcOrigin = 'https://keycloak.opentdf.local:9443/auth/realms/opentdf'; // Token provider function for client credentials async function getAccessToken() { + // In production, cache tokens and refresh only when expired. const resp = await fetch(`${oidcOrigin}/protocol/openid-connect/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: 'grant_type=client_credentials&client_id=opentdf&client_secret=secret', + body: new URLSearchParams({ + grant_type: 'client_credentials', + client_id: 'opentdf', + client_secret: 'secret', + }), }); const { access_token } = await resp.json(); return access_token; @@ -421,10 +431,15 @@ async function main() { // Token provider function for client credentials async function getAccessToken() { + // In production, cache tokens and refresh only when expired. const resp = await fetch(`${oidcOrigin}/protocol/openid-connect/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: 'grant_type=client_credentials&client_id=opentdf&client_secret=secret', + body: new URLSearchParams({ + grant_type: 'client_credentials', + client_id: 'opentdf', + client_secret: 'secret', + }), }); const { access_token } = await resp.json(); return access_token; diff --git a/docs/sdks/tdf.mdx b/docs/sdks/tdf.mdx index 6b900710..b413b86a 100644 --- a/docs/sdks/tdf.mdx +++ b/docs/sdks/tdf.mdx @@ -121,10 +121,15 @@ try (FileChannel ch = FileChannel.open(tmp, StandardOpenOption.READ)) { import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; async function getAccessToken(): Promise { + // In production, cache tokens and refresh only when expired. const resp = await fetch('https://platform.example.com/auth/realms/opentdf/protocol/openid-connect/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: 'grant_type=client_credentials&client_id=client-id&client_secret=client-secret', + body: new URLSearchParams({ + grant_type: 'client_credentials', + client_id: 'client-id', + client_secret: 'client-secret', + }), }); const { access_token } = await resp.json(); return access_token; From ff2f16146d815ee5a69e7c4bbf21e8fe1dff04aa Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 1 Apr 2026 10:21:10 -0700 Subject: [PATCH 04/12] fix(docs): use consistent link style for auth page references Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/authorization.mdx | 2 +- docs/sdks/discovery.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index 30af1ec3..8972adae 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -95,7 +95,7 @@ public class AuthorizationSetup { ```typescript // Option 1: Using interceptors (recommended for new code) -// getAccessToken() returns a valid access token — see /sdks/authentication +// getAccessToken() returns a valid access token — see the Authentication page import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; diff --git a/docs/sdks/discovery.mdx b/docs/sdks/discovery.mdx index 42baaa88..af1ee6c8 100644 --- a/docs/sdks/discovery.mdx +++ b/docs/sdks/discovery.mdx @@ -13,7 +13,7 @@ Before encrypting data with `CreateTDF`, it helps to verify that the attributes The Go, Java, and JavaScript SDKs provide five methods to discover and validate attributes without manual platform queries. :::tip JavaScript setup -The JS examples below assume you have a `getAccessToken()` function that returns a valid access token. See [Authentication](/sdks/authentication) for how to set this up. The `auth` object can be shared across all SDK calls — define it once, pass it everywhere. +The JS examples below assume you have a `getAccessToken()` function that returns a valid access token — see the [Authentication](/sdks/authentication) page for how to set this up. The `auth` object can be shared across all SDK calls — define it once, pass it everywhere. ::: --- From b807d77505559cf4d1cead71430d6c11b5327a69 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 1 Apr 2026 10:30:56 -0700 Subject: [PATCH 05/12] fix(docs): inline interceptor in code samples per review feedback Replace `...auth` spread (undefined in snippet context) with explicit `interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())]` in all policy code samples and policy.mdx JS blocks. Co-Authored-By: Claude Opus 4.6 (1M context) --- code_samples/policy_code/create_attribute.mdx | 3 +- code_samples/policy_code/create_namespace.mdx | 3 +- .../create_subject_condition_set.mdx | 3 +- .../policy_code/create_subject_mapping.mdx | 3 +- code_samples/policy_code/list_attributes.mdx | 3 +- code_samples/policy_code/list_namespaces.mdx | 3 +- .../policy_code/list_subject_mapping.mdx | 3 +- docs/sdks/policy.mdx | 150 +++++++++++++----- 8 files changed, 121 insertions(+), 50 deletions(-) diff --git a/code_samples/policy_code/create_attribute.mdx b/code_samples/policy_code/create_attribute.mdx index 314439bf..ebf50f5e 100644 --- a/code_samples/policy_code/create_attribute.mdx +++ b/code_samples/policy_code/create_attribute.mdx @@ -73,11 +73,12 @@ import CreateAttributeExample from '@site/code_samples/java/create-attribute.mdx ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; import { AttributeRuleTypeEnum } from '@opentdf/sdk/platform/policy/objects_pb.js'; const platform = new PlatformClient({ - ...auth, + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/create_namespace.mdx b/code_samples/policy_code/create_namespace.mdx index 2d1bded1..287503d4 100644 --- a/code_samples/policy_code/create_namespace.mdx +++ b/code_samples/policy_code/create_namespace.mdx @@ -54,10 +54,11 @@ import CreateNamespaceExample from '@site/code_samples/java/create-namespace.mdx ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - ...auth, + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/create_subject_condition_set.mdx b/code_samples/policy_code/create_subject_condition_set.mdx index 1e17fe35..2c550ae1 100644 --- a/code_samples/policy_code/create_subject_condition_set.mdx +++ b/code_samples/policy_code/create_subject_condition_set.mdx @@ -76,6 +76,7 @@ import CreateSubjectConditionSetExample from '@site/code_samples/java/create-sub ```typescript import { create } from '@bufbuild/protobuf'; +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; import { ConditionBooleanTypeEnum, @@ -86,7 +87,7 @@ import { } from '@opentdf/sdk/platform/policy/subjectmapping/subject_mapping_pb.js'; const platform = new PlatformClient({ - ...auth, + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/create_subject_mapping.mdx b/code_samples/policy_code/create_subject_mapping.mdx index 0276955f..54cf1543 100644 --- a/code_samples/policy_code/create_subject_mapping.mdx +++ b/code_samples/policy_code/create_subject_mapping.mdx @@ -63,10 +63,11 @@ import CreateSubjectMappingExample from '@site/code_samples/java/create-subject- ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - ...auth, + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/list_attributes.mdx b/code_samples/policy_code/list_attributes.mdx index 50088fa0..da7bee35 100644 --- a/code_samples/policy_code/list_attributes.mdx +++ b/code_samples/policy_code/list_attributes.mdx @@ -58,10 +58,11 @@ import ListAttributesExample from '@site/code_samples/java/list-attributes.mdx'; ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - ...auth, + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/list_namespaces.mdx b/code_samples/policy_code/list_namespaces.mdx index 76c24f83..e93f6eb6 100644 --- a/code_samples/policy_code/list_namespaces.mdx +++ b/code_samples/policy_code/list_namespaces.mdx @@ -53,10 +53,11 @@ import ListNamespacesExample from '@site/code_samples/java/list-namespaces.mdx'; ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - ...auth, + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/list_subject_mapping.mdx b/code_samples/policy_code/list_subject_mapping.mdx index 15dd0913..9be2ee03 100644 --- a/code_samples/policy_code/list_subject_mapping.mdx +++ b/code_samples/policy_code/list_subject_mapping.mdx @@ -56,10 +56,11 @@ import ListSubjectMappingsExample from '@site/code_samples/java/list-subject-map ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - ...auth, + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], platformUrl: 'http://localhost:8080', }); diff --git a/docs/sdks/policy.mdx b/docs/sdks/policy.mdx index 0662fabd..a3833d72 100644 --- a/docs/sdks/policy.mdx +++ b/docs/sdks/policy.mdx @@ -126,10 +126,13 @@ public class GetNamespaceExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); // Look up by UUID let resp = await platform.v1.namespace.getNamespace({ id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479' }); @@ -234,10 +237,13 @@ public class UpdateNamespaceExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.namespace.updateNamespace({ id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', @@ -322,10 +328,13 @@ public class DeactivateNamespaceExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); await platform.v1.namespace.deactivateNamespace({ id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479' }); console.log('Namespace deactivated.'); @@ -435,10 +444,13 @@ public class GetAttributeExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); // Look up by FQN const resp = await platform.v1.attributes.getAttribute({ @@ -536,10 +548,13 @@ public class UpdateAttributeExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.attributes.updateAttribute({ id: 'a1b2c3d4-0000-0000-0000-000000000001', @@ -624,10 +639,13 @@ public class DeactivateAttributeExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.attributes.deactivateAttribute({ id: 'a1b2c3d4-0000-0000-0000-000000000001', @@ -716,10 +734,13 @@ public class CreateAttributeValueExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.attributes.createAttributeValue({ attributeId: 'a1b2c3d4-0000-0000-0000-000000000001', @@ -807,10 +828,13 @@ public class ListAttributeValuesExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.attributes.listAttributeValues({ attributeId: 'a1b2c3d4-0000-0000-0000-000000000001', @@ -898,10 +922,13 @@ public class GetAttributeValueExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); // Look up by FQN const resp = await platform.v1.attributes.getAttributeValue({ @@ -1002,10 +1029,13 @@ public class GetAttributeValuesByFqnsExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.attributes.getAttributeValuesByFqns({ fqns: [ @@ -1104,10 +1134,13 @@ public class UpdateAttributeValueExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.attributes.updateAttributeValue({ id: 'v1b2c3d4-0000-0000-0000-000000000001', @@ -1192,10 +1225,13 @@ public class DeactivateAttributeValueExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.attributes.deactivateAttributeValue({ id: 'v1b2c3d4-0000-0000-0000-000000000001', @@ -1289,10 +1325,13 @@ public class ListSubjectConditionSetsExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.listSubjectConditionSets({}); for (const scs of resp.subjectConditionSets) { @@ -1380,10 +1419,13 @@ public class GetSubjectConditionSetExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.getSubjectConditionSet({ id: 'a0b1c2d3-0000-0000-0000-000000000099', @@ -1504,10 +1546,13 @@ public class UpdateSubjectConditionSetExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.updateSubjectConditionSet({ id: 'a0b1c2d3-0000-0000-0000-000000000099', @@ -1606,10 +1651,13 @@ public class DeleteSubjectConditionSetExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.deleteSubjectConditionSet({ id: 'a0b1c2d3-0000-0000-0000-000000000099', @@ -1689,10 +1737,13 @@ public class DeleteAllUnmappedSubjectConditionSetsExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.deleteAllUnmappedSubjectConditionSets({}); console.log(`Deleted ${resp.subjectConditionSets.length} unmapped subject condition sets.`); @@ -1786,10 +1837,13 @@ public class GetSubjectMappingExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.getSubjectMapping({ id: '890b26db-4ee4-447f-ae8a-2862d922eeef', @@ -1891,10 +1945,13 @@ public class UpdateSubjectMappingExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.updateSubjectMapping({ id: '890b26db-4ee4-447f-ae8a-2862d922eeef', @@ -1978,10 +2035,13 @@ public class DeleteSubjectMappingExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.deleteSubjectMapping({ id: '890b26db-4ee4-447f-ae8a-2862d922eeef', @@ -2082,10 +2142,13 @@ public class MatchSubjectMappingsExample { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -// See /sdks/platform-client for full setup including interceptor auth. -const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); +const platform = new PlatformClient({ + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + platformUrl: 'http://localhost:8080', +}); const resp = await platform.v1.subjectMapping.matchSubjectMappings({ subjectProperties: [ @@ -2128,10 +2191,11 @@ if err != nil { ```typescript +import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - ...auth, + interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], platformUrl: 'http://localhost:8080', }); From ba8b4b944da50821919553c506cc88d8eef9bc07 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 1 Apr 2026 10:37:34 -0700 Subject: [PATCH 06/12] fix(docs): use getAccessToken() instead of myAuth.getAccessToken() Co-Authored-By: Claude Opus 4.6 (1M context) --- code_samples/policy_code/create_attribute.mdx | 2 +- code_samples/policy_code/create_namespace.mdx | 2 +- .../create_subject_condition_set.mdx | 2 +- .../policy_code/create_subject_mapping.mdx | 2 +- code_samples/policy_code/list_attributes.mdx | 2 +- code_samples/policy_code/list_namespaces.mdx | 2 +- .../policy_code/list_subject_mapping.mdx | 2 +- docs/sdks/policy.mdx | 44 +++++++++---------- 8 files changed, 29 insertions(+), 29 deletions(-) diff --git a/code_samples/policy_code/create_attribute.mdx b/code_samples/policy_code/create_attribute.mdx index ebf50f5e..977ae2f7 100644 --- a/code_samples/policy_code/create_attribute.mdx +++ b/code_samples/policy_code/create_attribute.mdx @@ -78,7 +78,7 @@ import { PlatformClient } from '@opentdf/sdk/platform'; import { AttributeRuleTypeEnum } from '@opentdf/sdk/platform/policy/objects_pb.js'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/create_namespace.mdx b/code_samples/policy_code/create_namespace.mdx index 287503d4..1069a8b5 100644 --- a/code_samples/policy_code/create_namespace.mdx +++ b/code_samples/policy_code/create_namespace.mdx @@ -58,7 +58,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/create_subject_condition_set.mdx b/code_samples/policy_code/create_subject_condition_set.mdx index 2c550ae1..b18e9335 100644 --- a/code_samples/policy_code/create_subject_condition_set.mdx +++ b/code_samples/policy_code/create_subject_condition_set.mdx @@ -87,7 +87,7 @@ import { } from '@opentdf/sdk/platform/policy/subjectmapping/subject_mapping_pb.js'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/create_subject_mapping.mdx b/code_samples/policy_code/create_subject_mapping.mdx index 54cf1543..cca31fd4 100644 --- a/code_samples/policy_code/create_subject_mapping.mdx +++ b/code_samples/policy_code/create_subject_mapping.mdx @@ -67,7 +67,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/list_attributes.mdx b/code_samples/policy_code/list_attributes.mdx index da7bee35..88b423c8 100644 --- a/code_samples/policy_code/list_attributes.mdx +++ b/code_samples/policy_code/list_attributes.mdx @@ -62,7 +62,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/list_namespaces.mdx b/code_samples/policy_code/list_namespaces.mdx index e93f6eb6..fd9300a4 100644 --- a/code_samples/policy_code/list_namespaces.mdx +++ b/code_samples/policy_code/list_namespaces.mdx @@ -57,7 +57,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/list_subject_mapping.mdx b/code_samples/policy_code/list_subject_mapping.mdx index 9be2ee03..be2c259a 100644 --- a/code_samples/policy_code/list_subject_mapping.mdx +++ b/code_samples/policy_code/list_subject_mapping.mdx @@ -60,7 +60,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); diff --git a/docs/sdks/policy.mdx b/docs/sdks/policy.mdx index a3833d72..24d25bb5 100644 --- a/docs/sdks/policy.mdx +++ b/docs/sdks/policy.mdx @@ -130,7 +130,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -241,7 +241,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -332,7 +332,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -448,7 +448,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -552,7 +552,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -643,7 +643,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -738,7 +738,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -832,7 +832,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -926,7 +926,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -1033,7 +1033,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -1138,7 +1138,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -1229,7 +1229,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -1329,7 +1329,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -1423,7 +1423,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -1550,7 +1550,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -1655,7 +1655,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -1741,7 +1741,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -1841,7 +1841,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -1949,7 +1949,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -2039,7 +2039,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -2146,7 +2146,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); @@ -2195,7 +2195,7 @@ import { authTokenInterceptor } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => myAuth.getAccessToken())], + interceptors: [authTokenInterceptor(() => getAccessToken())], platformUrl: 'http://localhost:8080', }); From ce250de90c1e2f2f596d1ef883585395a4b46485 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Thu, 2 Apr 2026 15:20:46 -0700 Subject: [PATCH 07/12] feat(docs): use token provider factories in all JS examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace manual getAccessToken() implementations with the new clientCredentialsTokenProvider, refreshTokenProvider, and externalJwtTokenProvider factory functions from web-sdk#906. These handle caching, auto-refresh, and concurrent request deduplication out of the box — no more hand-rolled OAuth fetch code in every example. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- code_samples/policy_code/create_attribute.mdx | 7 +- code_samples/policy_code/create_namespace.mdx | 7 +- .../create_subject_condition_set.mdx | 7 +- .../policy_code/create_subject_mapping.mdx | 7 +- code_samples/policy_code/list_attributes.mdx | 7 +- code_samples/policy_code/list_namespaces.mdx | 7 +- .../policy_code/list_subject_mapping.mdx | 7 +- docs/guides/authentication-guide.mdx | 14 +- docs/sdks/authentication.mdx | 75 +++------ docs/sdks/authorization.mdx | 9 +- docs/sdks/discovery.mdx | 37 +++-- docs/sdks/platform-client.mdx | 25 +-- docs/sdks/policy.mdx | 154 +++++++++++++----- docs/sdks/quickstart/javascript.mdx | 73 +++------ docs/sdks/tdf.mdx | 40 +++-- docs/sdks/troubleshooting.mdx | 10 +- 16 files changed, 264 insertions(+), 222 deletions(-) diff --git a/code_samples/policy_code/create_attribute.mdx b/code_samples/policy_code/create_attribute.mdx index 977ae2f7..f809e107 100644 --- a/code_samples/policy_code/create_attribute.mdx +++ b/code_samples/policy_code/create_attribute.mdx @@ -73,12 +73,15 @@ import CreateAttributeExample from '@site/code_samples/java/create-attribute.mdx ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; import { AttributeRuleTypeEnum } from '@opentdf/sdk/platform/policy/objects_pb.js'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/create_namespace.mdx b/code_samples/policy_code/create_namespace.mdx index 1069a8b5..b6d3c28c 100644 --- a/code_samples/policy_code/create_namespace.mdx +++ b/code_samples/policy_code/create_namespace.mdx @@ -54,11 +54,14 @@ import CreateNamespaceExample from '@site/code_samples/java/create-namespace.mdx ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/create_subject_condition_set.mdx b/code_samples/policy_code/create_subject_condition_set.mdx index b18e9335..1da03c26 100644 --- a/code_samples/policy_code/create_subject_condition_set.mdx +++ b/code_samples/policy_code/create_subject_condition_set.mdx @@ -76,7 +76,7 @@ import CreateSubjectConditionSetExample from '@site/code_samples/java/create-sub ```typescript import { create } from '@bufbuild/protobuf'; -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; import { ConditionBooleanTypeEnum, @@ -87,7 +87,10 @@ import { } from '@opentdf/sdk/platform/policy/subjectmapping/subject_mapping_pb.js'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/create_subject_mapping.mdx b/code_samples/policy_code/create_subject_mapping.mdx index cca31fd4..e4742821 100644 --- a/code_samples/policy_code/create_subject_mapping.mdx +++ b/code_samples/policy_code/create_subject_mapping.mdx @@ -63,11 +63,14 @@ import CreateSubjectMappingExample from '@site/code_samples/java/create-subject- ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/list_attributes.mdx b/code_samples/policy_code/list_attributes.mdx index 88b423c8..157897e1 100644 --- a/code_samples/policy_code/list_attributes.mdx +++ b/code_samples/policy_code/list_attributes.mdx @@ -58,11 +58,14 @@ import ListAttributesExample from '@site/code_samples/java/list-attributes.mdx'; ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/list_namespaces.mdx b/code_samples/policy_code/list_namespaces.mdx index fd9300a4..39a6dd6c 100644 --- a/code_samples/policy_code/list_namespaces.mdx +++ b/code_samples/policy_code/list_namespaces.mdx @@ -53,11 +53,14 @@ import ListNamespacesExample from '@site/code_samples/java/list-namespaces.mdx'; ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/code_samples/policy_code/list_subject_mapping.mdx b/code_samples/policy_code/list_subject_mapping.mdx index be2c259a..8f749add 100644 --- a/code_samples/policy_code/list_subject_mapping.mdx +++ b/code_samples/policy_code/list_subject_mapping.mdx @@ -56,11 +56,14 @@ import ListSubjectMappingsExample from '@site/code_samples/java/list-subject-map ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/docs/guides/authentication-guide.mdx b/docs/guides/authentication-guide.mdx index 95ed9c5e..3ab87261 100644 --- a/docs/guides/authentication-guide.mdx +++ b/docs/guides/authentication-guide.mdx @@ -61,8 +61,8 @@ flowchart TD | Scenario | Recommended Method | Notes | |----------|-------------------|-------| -| Backend service / microservice | [Client Credentials](/sdks/authentication#client-credentials) | Most straightforward. SDK auto-refreshes tokens on expiry. | -| Web application (browser) | [OIDC Login Flow](/sdks/authentication#refresh-token) | JS SDK only. Your app completes an OIDC login to get an access token and optional refresh token. Never expose client secrets in browser code. | +| Backend service / microservice | [Client Credentials](/sdks/authentication#client-credentials) | Most straightforward. Use `clientCredentialsTokenProvider()` — it caches tokens and auto-refreshes on expiry. | +| Web application (browser) | [OIDC Login Flow](/sdks/authentication#refresh-token) | JS SDK only. Use `refreshTokenProvider()` with a refresh token from your OIDC login flow. Never expose client secrets in browser code. | | CLI tool (interactive user) | `otdfctl auth login` | Opens a browser for OIDC login. See [CLI auth docs](/components/cli/auth/login). | | CI/CD pipeline / automated job | [Client Credentials](/sdks/authentication#client-credentials) | Use a dedicated service account. Rotate secrets regularly. | | Federated identity / SAML | [Token Exchange](/sdks/authentication#token-exchange) | Exchange an existing token for one the platform accepts. | @@ -73,7 +73,7 @@ flowchart TD ### Client Credentials (Backend Services) -The simplest path. Register a client in your IdP, then pass the client ID and secret to the SDK. The SDK handles token acquisition and refresh automatically. +The simplest path. Register a client in your IdP, then use `clientCredentialsTokenProvider()` (JS) or pass the client ID and secret directly (Go/Java). The SDK handles token acquisition, caching, and refresh automatically. **What you need:** A client ID and client secret from your IdP. @@ -81,9 +81,9 @@ See [SDK code examples](/sdks/authentication#client-credentials) for Go, Java, a ### OIDC Login Flow (Browser Apps) -For browser-based applications, your app completes an [OIDC Authorization Code flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) to obtain an access token and (optionally) a refresh token from the IdP. The IdP returns both tokens after a successful `/token` call with a valid authorization code — though the refresh token is only issued if the IdP is configured to provide one. You then pass the refresh token to the JS SDK, which uses it to maintain a valid access token. +For browser-based applications, your app completes an [OIDC Authorization Code flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) to obtain an access token and (optionally) a refresh token from the IdP. The IdP returns both tokens after a successful `/token` call with a valid authorization code — though the refresh token is only issued if the IdP is configured to provide one. You then pass the refresh token to `refreshTokenProvider()`, which handles token exchange and automatic refresh. -**What you need:** An access token and optional refresh token from a completed OIDC login flow. +**What you need:** A refresh token from a completed OIDC login flow. See [SDK code examples](/sdks/authentication#refresh-token) (JavaScript only). @@ -165,9 +165,9 @@ OpenTDF does not prescribe a specific deployment strategy. This checklist covers ### "My token expired and I'm getting errors" -- **Client credentials:** The SDK refreshes automatically — if you're seeing expiry errors, check that your IdP is reachable. +- **Client credentials:** The built-in `clientCredentialsTokenProvider()` (JS) and Go/Java SDKs refresh automatically — if you're seeing expiry errors, check that your IdP is reachable. - **OAuth token source (Go):** `WithOAuthAccessTokenSource` does not auto-refresh. Your `TokenSource` implementation must handle refresh. -- **Refresh token (JS):** Ensure the refresh token itself hasn't expired. Long-lived refresh tokens can be configured in your IdP. +- **Refresh token (JS):** `refreshTokenProvider()` handles rotation automatically, but ensure the initial refresh token itself hasn't expired. Long-lived refresh tokens can be configured in your IdP. ### "DPoP-related errors" diff --git a/docs/sdks/authentication.mdx b/docs/sdks/authentication.mdx index 844bfb6b..6c242831 100644 --- a/docs/sdks/authentication.mdx +++ b/docs/sdks/authentication.mdx @@ -48,28 +48,14 @@ SDK sdk = new SDKBuilder() ```typescript -import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; - -// Use your preferred OAuth2 library to obtain tokens via client credentials grant. -// The interceptor calls your token provider per-request. -async function getAccessToken(): Promise { - // In production, cache tokens and refresh only when expired. - // Example: fetch token from your IdP's token endpoint - const resp = await fetch('http://localhost:8080/auth/realms/opentdf/protocol/openid-connect/token', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - grant_type: 'client_credentials', - client_id: 'my-client-id', - client_secret: 'my-client-secret', - }), - }); - const { access_token } = await resp.json(); - return access_token; -} +import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; const client = new OpenTDF({ - interceptors: [authTokenInterceptor(getAccessToken)], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); ``` @@ -81,7 +67,7 @@ Client credentials should be used only in Node.js or server-side environments. N -**Token lifecycle:** The SDK calls your token provider on each request. Your provider is responsible for caching and refreshing tokens as needed. +**Token lifecycle:** The built-in token providers automatically cache tokens and refresh them when they expire (with a 30-second buffer). You can also write your own `TokenProvider` — any `() => Promise` function works. ## Token Exchange @@ -120,28 +106,14 @@ The Java SDK wraps the JWT as a `BearerAccessToken` and performs an RFC 8693 tok ```typescript -import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; - -// Exchange your external JWT for a platform token, then provide it to the SDK. -async function getExchangedToken(): Promise { - // In production, cache tokens and refresh only when expired. - // Perform RFC 8693 token exchange with your IdP - const resp = await fetch('http://localhost:8080/auth/realms/opentdf/protocol/openid-connect/token', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', - subject_token: 'eyJhbGciOi...', - subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', - client_id: 'my-client-id', - }), - }); - const { access_token } = await resp.json(); - return access_token; -} +import { authTokenInterceptor, externalJwtTokenProvider, OpenTDF } from '@opentdf/sdk'; const client = new OpenTDF({ - interceptors: [authTokenInterceptor(getExchangedToken)], + interceptors: [authTokenInterceptor(externalJwtTokenProvider({ + clientId: 'my-client-id', + externalJwt: 'eyJhbGciOi...', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); ``` @@ -158,13 +130,16 @@ Refresh token authentication is currently available in the JavaScript SDK only. ::: ```typescript -import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, refreshTokenProvider, OpenTDF } from '@opentdf/sdk'; -// For browser apps: use your OIDC library's token refresh mechanism. -// The interceptor calls your provider per-request — it should return a valid token, -// refreshing automatically when needed. +// Use a refresh token obtained from a prior OIDC login flow. +// The provider automatically exchanges it for access tokens and handles rotation. const client = new OpenTDF({ - interceptors: [authTokenInterceptor(() => myOidcClient.getAccessToken())], + interceptors: [authTokenInterceptor(refreshTokenProvider({ + clientId: 'my-app', + refreshToken: 'refresh-token-from-login-flow', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); ``` @@ -313,10 +288,14 @@ const client = new OpenTDF({ ```typescript -import { authTokenDPoPInterceptor, OpenTDF } from '@opentdf/sdk'; +import { authTokenDPoPInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; const dpopInterceptor = authTokenDPoPInterceptor({ - tokenProvider: () => getAccessToken(), + tokenProvider: clientCredentialsTokenProvider({ + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }), }); const client = new OpenTDF({ diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index 8972adae..8803d43d 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -95,12 +95,15 @@ public class AuthorizationSetup { ```typescript // Option 1: Using interceptors (recommended for new code) -// getAccessToken() returns a valid access token — see the Authentication page -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platformClient = new PlatformClient({ - interceptors: [authTokenInterceptor(getAccessToken)], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', + clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/docs/sdks/discovery.mdx b/docs/sdks/discovery.mdx index af1ee6c8..9f23f3a9 100644 --- a/docs/sdks/discovery.mdx +++ b/docs/sdks/discovery.mdx @@ -13,7 +13,7 @@ Before encrypting data with `CreateTDF`, it helps to verify that the attributes The Go, Java, and JavaScript SDKs provide five methods to discover and validate attributes without manual platform queries. :::tip JavaScript setup -The JS examples below assume you have a `getAccessToken()` function that returns a valid access token — see the [Authentication](/sdks/authentication) page for how to set this up. The `auth` object can be shared across all SDK calls — define it once, pass it everywhere. +The JS examples below use `clientCredentialsTokenProvider` for authentication — see the [Authentication](/sdks/authentication) page for all available token providers. The `auth` object can be shared across all SDK calls — define it once, pass it everywhere. ::: --- @@ -69,9 +69,12 @@ List attrs = sdk.listAttributes("opentdf.io"); ```ts -import { authTokenInterceptor, listAttributes } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, listAttributes } from '@opentdf/sdk'; -const auth = { interceptors: [authTokenInterceptor(getAccessToken)] }; +const auth = { interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', +}))] }; const attrs = await listAttributes(platformUrl, auth); @@ -133,9 +136,12 @@ if (!exists) { ```ts -import { authTokenInterceptor, attributeExists } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, attributeExists } from '@opentdf/sdk'; -const auth = { interceptors: [authTokenInterceptor(getAccessToken)] }; +const auth = { interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', +}))] }; const exists = await attributeExists(platformUrl, auth, 'https://opentdf.io/attr/department'); if (!exists) { @@ -185,9 +191,12 @@ if (!exists) { ```ts -import { authTokenInterceptor, attributeValueExists } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, attributeValueExists } from '@opentdf/sdk'; -const auth = { interceptors: [authTokenInterceptor(getAccessToken)] }; +const auth = { interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', +}))] }; const exists = await attributeValueExists(platformUrl, auth, 'https://opentdf.io/attr/department/value/finance'); if (!exists) { @@ -255,9 +264,12 @@ sdk.createTDF(inputStream, outputStream, config); ```ts -import { authTokenInterceptor, validateAttributes, AttributeNotFoundError } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, validateAttributes, AttributeNotFoundError } from '@opentdf/sdk'; -const auth = { interceptors: [authTokenInterceptor(getAccessToken)] }; +const auth = { interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', +}))] }; const fqns = [ 'https://opentdf.io/attr/department/value/marketing', @@ -507,10 +519,13 @@ try (SDK sdk = new SDKBuilder() ```ts -import { authTokenInterceptor, OpenTDF, listAttributes, attributeExists, validateAttributes, AttributeNotFoundError } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF, listAttributes, attributeExists, validateAttributes, AttributeNotFoundError } from '@opentdf/sdk'; // Define auth once — reuse for standalone functions and the OpenTDF client -const auth = { interceptors: [authTokenInterceptor(getAccessToken)] }; +const auth = { interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: oidcEndpoint, +}))] }; // 1. See what's available on the platform const attrs = await listAttributes(platformUrl, auth); diff --git a/docs/sdks/platform-client.mdx b/docs/sdks/platform-client.mdx index 2d5f0ba7..ef494aa7 100644 --- a/docs/sdks/platform-client.mdx +++ b/docs/sdks/platform-client.mdx @@ -62,25 +62,16 @@ SDK sdk = SDKBuilder.newBuilder() ```typescript -import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; -async function getAccessToken(): Promise { - // In production, cache tokens and refresh only when expired. - const resp = await fetch('http://localhost:8080/auth/realms/opentdf/protocol/openid-connect/token', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - grant_type: 'client_credentials', - client_id: 'client-id', - client_secret: 'client-secret', - }), - }); - const { access_token } = await resp.json(); - return access_token; -} - -const auth = { interceptors: [authTokenInterceptor(getAccessToken)] }; +const auth = { + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'client-id', + clientSecret: 'client-secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], +}; const client = new OpenTDF({ ...auth, platformUrl: 'http://localhost:8080' }); const platform = new PlatformClient({ ...auth, platformUrl: 'http://localhost:8080' }); diff --git a/docs/sdks/policy.mdx b/docs/sdks/policy.mdx index 24d25bb5..69d4eac6 100644 --- a/docs/sdks/policy.mdx +++ b/docs/sdks/policy.mdx @@ -126,11 +126,14 @@ public class GetNamespaceExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -237,11 +240,14 @@ public class UpdateNamespaceExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -328,11 +334,14 @@ public class DeactivateNamespaceExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -444,11 +453,14 @@ public class GetAttributeExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -548,11 +560,14 @@ public class UpdateAttributeExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -639,11 +654,14 @@ public class DeactivateAttributeExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -734,11 +752,14 @@ public class CreateAttributeValueExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -828,11 +849,14 @@ public class ListAttributeValuesExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -922,11 +946,14 @@ public class GetAttributeValueExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -1029,11 +1056,14 @@ public class GetAttributeValuesByFqnsExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -1134,11 +1164,14 @@ public class UpdateAttributeValueExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -1225,11 +1258,14 @@ public class DeactivateAttributeValueExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -1325,11 +1361,14 @@ public class ListSubjectConditionSetsExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -1419,11 +1458,14 @@ public class GetSubjectConditionSetExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -1546,11 +1588,14 @@ public class UpdateSubjectConditionSetExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -1651,11 +1696,14 @@ public class DeleteSubjectConditionSetExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -1737,11 +1785,14 @@ public class DeleteAllUnmappedSubjectConditionSetsExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -1837,11 +1888,14 @@ public class GetSubjectMappingExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -1945,11 +1999,14 @@ public class UpdateSubjectMappingExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -2035,11 +2092,14 @@ public class DeleteSubjectMappingExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -2142,11 +2202,14 @@ public class MatchSubjectMappingsExample { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); @@ -2191,11 +2254,14 @@ if err != nil { ```typescript -import { authTokenInterceptor } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platform = new PlatformClient({ - interceptors: [authTokenInterceptor(() => getAccessToken())], + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], platformUrl: 'http://localhost:8080', }); diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index 8b7a663d..e31dec2c 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -70,7 +70,7 @@ Expected output: Create a file named `index.mjs`: ```javascript title="index.mjs" -import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; async function main() { console.log('🚀 Starting OpenTDF SDK Quickstart...'); @@ -79,25 +79,16 @@ async function main() { const oidcOrigin = 'https://keycloak.opentdf.local:9443/auth/realms/opentdf'; console.log(`📡 Connecting to platform: ${platformUrl}`); - // Token provider function for client credentials - async function getAccessToken() { - // In production, cache tokens and refresh only when expired. - const resp = await fetch(`${oidcOrigin}/protocol/openid-connect/token`, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - grant_type: 'client_credentials', - client_id: 'opentdf', - client_secret: 'secret', - }), - }); - const { access_token } = await resp.json(); - return access_token; - } + // Create a token provider that handles caching and auto-refresh + const tokenProvider = clientCredentialsTokenProvider({ + clientId: 'opentdf', + clientSecret: 'secret', + oidcOrigin, + }); // Create a new OpenTDF client console.log('🔐 Initializing client with auth interceptor...'); - const client = new OpenTDF({ interceptors: [authTokenInterceptor(getAccessToken)], platformUrl }); + const client = new OpenTDF({ interceptors: [authTokenInterceptor(tokenProvider)], platformUrl }); console.log('✅ SDK client initialized successfully'); // Encrypt data @@ -194,31 +185,19 @@ For additional policy management examples including managing attributes, namespa Let's work with the "department" attribute and add a "marketing" value. If you completed the [Quickstart setup guide](/quickstart), the attribute may already exist with finance and engineering values. If not, we'll create it: ```javascript -import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; import { AttributeRuleTypeEnum } from '@opentdf/sdk/platform/policy/objects_pb.js'; const platformUrl = 'https://platform.opentdf.local:8443'; const oidcOrigin = 'https://keycloak.opentdf.local:9443/auth/realms/opentdf'; -// Token provider function for client credentials -async function getAccessToken() { - // In production, cache tokens and refresh only when expired. - const resp = await fetch(`${oidcOrigin}/protocol/openid-connect/token`, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - grant_type: 'client_credentials', - client_id: 'opentdf', - client_secret: 'secret', - }), - }); - const { access_token } = await resp.json(); - return access_token; -} - // Create the OpenTDF and Platform clients with auth interceptor -const interceptors = [authTokenInterceptor(getAccessToken)]; +const interceptors = [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', + clientSecret: 'secret', + oidcOrigin, +}))]; const client = new OpenTDF({ interceptors, platformUrl }); const platform = new PlatformClient({ interceptors, platformUrl }); @@ -417,7 +396,7 @@ For reference, here's a complete example showing all the pieces together: ```javascript title="index.mjs" import fs from 'node:fs'; -import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; import { AttributeRuleTypeEnum, @@ -429,24 +408,12 @@ async function main() { const platformUrl = 'https://platform.opentdf.local:8443'; const oidcOrigin = 'https://keycloak.opentdf.local:9443/auth/realms/opentdf'; - // Token provider function for client credentials - async function getAccessToken() { - // In production, cache tokens and refresh only when expired. - const resp = await fetch(`${oidcOrigin}/protocol/openid-connect/token`, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - grant_type: 'client_credentials', - client_id: 'opentdf', - client_secret: 'secret', - }), - }); - const { access_token } = await resp.json(); - return access_token; - } - // Create the OpenTDF and Platform clients with auth interceptor - const interceptors = [authTokenInterceptor(getAccessToken)]; + const interceptors = [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', + clientSecret: 'secret', + oidcOrigin, + }))]; const client = new OpenTDF({ interceptors, platformUrl }); const platform = new PlatformClient({ interceptors, platformUrl }); diff --git a/docs/sdks/tdf.mdx b/docs/sdks/tdf.mdx index b413b86a..8b52870d 100644 --- a/docs/sdks/tdf.mdx +++ b/docs/sdks/tdf.mdx @@ -118,24 +118,16 @@ try (FileChannel ch = FileChannel.open(tmp, StandardOpenOption.READ)) { ```typescript -import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; - -async function getAccessToken(): Promise { - // In production, cache tokens and refresh only when expired. - const resp = await fetch('https://platform.example.com/auth/realms/opentdf/protocol/openid-connect/token', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - grant_type: 'client_credentials', - client_id: 'client-id', - client_secret: 'client-secret', - }), - }); - const { access_token } = await resp.json(); - return access_token; -} - -const client = new OpenTDF({ interceptors: [authTokenInterceptor(getAccessToken)], platformUrl: 'https://platform.example.com' }); +import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; + +const client = new OpenTDF({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'client-id', + clientSecret: 'client-secret', + oidcOrigin: 'https://platform.example.com/auth/realms/opentdf', + }))], + platformUrl: 'https://platform.example.com', +}); // Encrypt const enc = new TextEncoder(); @@ -951,9 +943,15 @@ for _, fqn := range obligations.FQNs { ```typescript -import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; - -const client = new OpenTDF({ interceptors: [authTokenInterceptor(getAccessToken)], platformUrl: 'http://localhost:8080' }); +import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; + +const client = new OpenTDF({ + interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))], + platformUrl: 'http://localhost:8080', +}); const reader = client.open({ source: ciphertextSource }); const obligations = await reader.obligations(); diff --git a/docs/sdks/troubleshooting.mdx b/docs/sdks/troubleshooting.mdx index 2249472f..3070fae6 100644 --- a/docs/sdks/troubleshooting.mdx +++ b/docs/sdks/troubleshooting.mdx @@ -41,12 +41,14 @@ curl https:/// otdfctl auth client-credentials ``` -2. **Implement token refresh in your application**: Use `authTokenInterceptor` with a function that returns a fresh token on each call. This ensures the SDK always has a valid token: +2. **Use a built-in token provider**: The SDK's token providers handle caching and auto-refresh automatically: ```js - import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; + import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; - // getAccessToken should fetch/refresh a token from your IdP - const auth = { interceptors: [authTokenInterceptor(getAccessToken)] }; + const auth = { interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ + clientId: 'opentdf', clientSecret: 'secret', + oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', + }))] }; const client = new OpenTDF({ ...auth, platformUrl }); ``` From 0353a0960cd1ea8b3a88a8753add9de05df969fd Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Mon, 6 Apr 2026 11:50:53 -0700 Subject: [PATCH 08/12] fix(docs): add browser-use caveats for clientCredentialsTokenProvider examples The JS SDK is designed for browser apps, but all examples use clientCredentialsTokenProvider (which requires a client secret). Add a shared admonition across SDK doc pages clarifying this is for learning only and production browser apps should use refreshTokenProvider() with an OIDC login flow. Co-Authored-By: Claude Opus 4.6 (1M context) --- code_samples/js_auth_note.mdx | 5 +++++ docs/sdks/authentication.mdx | 6 +++--- docs/sdks/authorization.mdx | 3 +++ docs/sdks/discovery.mdx | 5 ++--- docs/sdks/platform-client.mdx | 3 +++ docs/sdks/policy.mdx | 3 +++ docs/sdks/quickstart/javascript.mdx | 4 ++++ docs/sdks/tdf.mdx | 3 +++ docs/sdks/troubleshooting.mdx | 3 +++ 9 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 code_samples/js_auth_note.mdx diff --git a/code_samples/js_auth_note.mdx b/code_samples/js_auth_note.mdx new file mode 100644 index 00000000..bee547b4 --- /dev/null +++ b/code_samples/js_auth_note.mdx @@ -0,0 +1,5 @@ +:::info JavaScript examples use client credentials for simplicity +The JavaScript SDK is designed for **browser applications**. The examples on this page use `clientCredentialsTokenProvider` because it's self-contained and easy to follow, but it requires a client secret and **must not be used in browser code**. + +In production browser apps, complete an OIDC login flow to obtain a refresh token, then use [`refreshTokenProvider()`](/sdks/authentication#refresh-token). See the [Authentication Decision Guide](/guides/authentication-guide) for help choosing the right method. +::: diff --git a/docs/sdks/authentication.mdx b/docs/sdks/authentication.mdx index 4a0e106d..aeac543c 100644 --- a/docs/sdks/authentication.mdx +++ b/docs/sdks/authentication.mdx @@ -11,7 +11,7 @@ import TabItem from '@theme/TabItem'; The OpenTDF SDKs authenticate with an [OIDC](https://openid.net/developers/how-connect-works/)-compatible identity provider (IdP) to obtain access tokens for the platform. The platform itself is a **resource server**, not an identity provider — you bring your own IdP (Keycloak is the reference implementation). :::tip Not sure which method to use? -See the [Authentication Decision Guide](/guides/authentication-guide) to choose the right approach for your use case. +The JavaScript SDK is designed for **browser applications**. For browser apps, start with [Refresh Token](#refresh-token) (the most common browser pattern). For backend scripts and testing, see [Client Credentials](#client-credentials). See the [Authentication Decision Guide](/guides/authentication-guide) for a full comparison. ::: ## Client Credentials @@ -60,8 +60,8 @@ const client = new OpenTDF({ }); ``` -:::warning Confidential clients only -Client credentials should be used only in Node.js or server-side environments. Never expose client secrets in browser code. +:::warning Server-side only — not for browsers +`clientCredentialsTokenProvider` requires a client secret and is intended for server-side scripts, CLI tools, and CI/CD pipelines. **Never expose client secrets in browser code.** The JavaScript SDK is designed for browser applications — in production, use [`refreshTokenProvider()`](#refresh-token) with a token from your OIDC login flow instead. ::: diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index 8803d43d..82898326 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -5,6 +5,7 @@ title: Authorization import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import JsAuthNote from '../../code_samples/js_auth_note.mdx' # Making Authorization Decisions @@ -27,6 +28,8 @@ OpenTDF's authorization system provides two primary methods for access control: All authorization calls require proper authentication. Here's how to set up the SDK client: + + diff --git a/docs/sdks/discovery.mdx b/docs/sdks/discovery.mdx index 9f23f3a9..0468b9c9 100644 --- a/docs/sdks/discovery.mdx +++ b/docs/sdks/discovery.mdx @@ -5,6 +5,7 @@ title: Discovery import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import JsAuthNote from '../../code_samples/js_auth_note.mdx' # Discovery @@ -12,9 +13,7 @@ Before encrypting data with `CreateTDF`, it helps to verify that the attributes The Go, Java, and JavaScript SDKs provide five methods to discover and validate attributes without manual platform queries. -:::tip JavaScript setup -The JS examples below use `clientCredentialsTokenProvider` for authentication — see the [Authentication](/sdks/authentication) page for all available token providers. The `auth` object can be shared across all SDK calls — define it once, pass it everywhere. -::: + --- diff --git a/docs/sdks/platform-client.mdx b/docs/sdks/platform-client.mdx index ef494aa7..454b98c2 100644 --- a/docs/sdks/platform-client.mdx +++ b/docs/sdks/platform-client.mdx @@ -5,6 +5,7 @@ title: Architecture import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import JsAuthNote from '../../code_samples/js_auth_note.mdx' # Architecture @@ -24,6 +25,8 @@ This is the same pattern used by cloud provider SDKs — you instantiate a typed ## Initializing the SDK client + + diff --git a/docs/sdks/policy.mdx b/docs/sdks/policy.mdx index 69d4eac6..892a9c51 100644 --- a/docs/sdks/policy.mdx +++ b/docs/sdks/policy.mdx @@ -12,11 +12,14 @@ import ListAttributes from '../../code_samples/policy_code/list_attributes.mdx' import CreateConditionSet from '../../code_samples/policy_code/create_subject_condition_set.mdx' import CreateSubjectMapping from '../../code_samples/policy_code/create_subject_mapping.mdx' import ListSubjectMapping from '../../code_samples/policy_code/list_subject_mapping.mdx' +import JsAuthNote from '../../code_samples/js_auth_note.mdx' # Managing Policy Policy in OpenTDF is the set of rules that govern who can access data and under what conditions. It is made up of **namespaces**, **attributes**, **subject mappings**, and **subject condition sets**. The SDK provides CRUD access to these policy rules through remote gRPC calls powered by the [platform service client](/sdks/platform-client). + + --- ## Namespaces diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index e31dec2c..8aca0cbd 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -65,6 +65,10 @@ Expected output: ## Step 3: Create Your Application {#step-3-js} +:::note Local development only +This quickstart uses `clientCredentialsTokenProvider` with the pre-configured Keycloak client (`opentdf`/`secret`) so you can focus on learning the SDK. In production browser apps, you would complete an OIDC login flow and pass the resulting refresh token to [`refreshTokenProvider()`](/sdks/authentication#refresh-token) instead — the JavaScript SDK is designed for browser use. See [Authentication](/sdks/authentication) for all options. +::: + ### JavaScript Implementation Code Create a file named `index.mjs`: diff --git a/docs/sdks/tdf.mdx b/docs/sdks/tdf.mdx index 29e4e331..d09d244c 100644 --- a/docs/sdks/tdf.mdx +++ b/docs/sdks/tdf.mdx @@ -8,6 +8,7 @@ import TabItem from '@theme/TabItem'; import EncryptOptions from '../../code_samples/tdf/encrypt_options.mdx' import DecryptOptions from '../../code_samples/tdf/decrypt_options.mdx' import AssertionExamples from '../../code_samples/tdf/assertion_examples.mdx' +import JsAuthNote from '../../code_samples/js_auth_note.mdx' # TDF @@ -31,6 +32,8 @@ This page covers the core TDF operations: ## Quick Start + + Initialize a client and run an end-to-end encrypt/decrypt in one block. diff --git a/docs/sdks/troubleshooting.mdx b/docs/sdks/troubleshooting.mdx index 3070fae6..191534af 100644 --- a/docs/sdks/troubleshooting.mdx +++ b/docs/sdks/troubleshooting.mdx @@ -5,6 +5,7 @@ title: Troubleshooting import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import JsAuthNote from '../../code_samples/js_auth_note.mdx' # SDK Troubleshooting @@ -52,6 +53,8 @@ curl https:/// const client = new OpenTDF({ ...auth, platformUrl }); ``` + + 3. **Re-instantiate the SDK**: For Go and Java, create a new SDK instance with fresh credentials when a token error occurs. ## Certificate Errors (SSL/TLS) From f35c2562c115506fa07aa4a88e89f260a29b88a3 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Mon, 6 Apr 2026 11:54:04 -0700 Subject: [PATCH 09/12] fix(docs): clarify JS SDK is browser-first in quickstart Step 1 Reframe the Node.js project setup as a convenience for the quickstart, not the intended runtime. The SDK targets browser apps (React, Angular, Vue, etc.). Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/quickstart/javascript.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index 8aca0cbd..f4bf8a37 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -34,7 +34,7 @@ See the [Managing the Platform](/getting-started/managing-platform) guide for de ## Step 1: Create a New Project {#step-1-js} -Create a new directory and initialize a Node.js project: +The JavaScript SDK is designed for **browser-based applications** — React, Angular, Vue, or any web app that runs in the browser. This quickstart uses Node.js so you can get a working project running quickly without scaffolding a full web app, but in production you'd integrate the SDK into your frontend build. ```bash mkdir opentdf-quickstart From cbb2ca43706083c341d3d4469cf30de91cc7541c Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Mon, 6 Apr 2026 12:40:54 -0700 Subject: [PATCH 10/12] fix(docs): improve DX across SDK pages - Quickstart: frame Node.js as convenience, SDK is browser-first - Quickstart: use attributeExists/attributeValueExists discovery helpers - Quickstart: collapse nested subject condition set arrays - Authentication: define getMyToken(), add Go/Java DPoP tabs, fix mTLS - Platform client: show Go service usage, use authorization v2 - TDF: per-SDK parameter tables instead of ambiguous shared table - Troubleshooting: link to all 3 SDK repos for filing issues Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/authentication.mdx | 53 +++++++- docs/sdks/platform-client.mdx | 9 +- docs/sdks/quickstart/javascript.mdx | 180 ++++++++++++---------------- docs/sdks/tdf.mdx | 32 ++++- docs/sdks/troubleshooting.mdx | 7 +- 5 files changed, 168 insertions(+), 113 deletions(-) diff --git a/docs/sdks/authentication.mdx b/docs/sdks/authentication.mdx index aeac543c..c941aaf4 100644 --- a/docs/sdks/authentication.mdx +++ b/docs/sdks/authentication.mdx @@ -196,9 +196,7 @@ Java's TLS configuration is layered on top of another auth method (typically cli -The JavaScript SDK relies on the runtime's TLS configuration (browser or Node.js). Configure client certificates at the environment level rather than in the SDK. - -For Node.js, use the `NODE_EXTRA_CA_CERTS` environment variable or configure the `https` agent. +In browsers, client certificates are managed by the operating system and browser — the user is prompted to select a certificate when the server requests one. No SDK configuration is needed. @@ -255,12 +253,21 @@ SDK sdk = new SDKBuilder() -Write a custom interceptor for full control over request authentication: +Write a custom interceptor for full control over request authentication. This is useful when your app already has its own auth system (e.g., an OIDC library like `oidc-client-ts`, Auth0, or a custom token store): ```typescript import { type Interceptor } from '@connectrpc/connect'; import { OpenTDF } from '@opentdf/sdk'; +// Replace this with however your app obtains tokens — +// e.g., from an OIDC library, auth context, or token store. +async function getMyToken(): Promise { + // Example: read from your OIDC library's user manager + // const user = await userManager.getUser(); + // return user?.access_token ?? ''; + throw new Error('Implement getMyToken() for your auth system'); +} + const myAuthInterceptor: Interceptor = (next) => async (req) => { req.header.set('Authorization', `Bearer ${await getMyToken()}`); return next(req); @@ -286,7 +293,37 @@ const client = new OpenTDF({ | **JavaScript** | Off by default with interceptors | Use `authTokenDPoPInterceptor()` to enable | - + + +DPoP is enabled by default with an auto-generated key. To provide your own RSA key: + +```go +import "crypto/rsa" + +// Provide your own RSA key for DPoP signing +client, err := sdk.New("http://localhost:8080", + sdk.WithClientCredentials("my-client-id", "my-client-secret", nil), + sdk.WithSessionSignerRSA(myRSAPrivateKey), // *rsa.PrivateKey +) +``` + + + + +DPoP is always on with an auto-generated RSA key. To provide a custom signer: + +```java +SDK sdk = new SDKBuilder() + .platformEndpoint("http://localhost:8080") + .clientSecret("my-client-id", "my-client-secret") + .srtSigner(customSigner) // custom DPoP signer + .build(); +``` + + + + +DPoP is off by default with interceptors. Use `authTokenDPoPInterceptor()` to enable it: ```typescript import { authTokenDPoPInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; @@ -321,6 +358,9 @@ The `AuthProvider` interface is deprecated as of the interceptor-based auth intr The legacy `AuthProvider` pattern managed token lifecycle internally: + + + ```typescript import { AuthProviders, OpenTDF } from '@opentdf/sdk'; @@ -337,3 +377,6 @@ const client = new OpenTDF({ }); await client.ready; ``` + + + diff --git a/docs/sdks/platform-client.mdx b/docs/sdks/platform-client.mdx index 454b98c2..45f31aa3 100644 --- a/docs/sdks/platform-client.mdx +++ b/docs/sdks/platform-client.mdx @@ -35,7 +35,7 @@ import ( "github.com/opentdf/platform/sdk" // Plus the service-specific package for each call, e.g.: "github.com/opentdf/platform/protocol/go/policy/namespaces" - "github.com/opentdf/platform/protocol/go/authorization" + authorization "github.com/opentdf/platform/protocol/go/authorization/v2" ) client, err := sdk.New("http://localhost:8080", @@ -45,6 +45,13 @@ if err != nil { log.Fatal(err) } defer client.Close() + +// Platform services are accessed directly on the client, e.g.: +resp, err := client.Namespaces.ListNamespaces(ctx, &namespaces.ListNamespacesRequest{}) + +decision, err := client.AuthorizationV2.GetDecision(ctx, &authorization.GetDecisionRequest{ + // ... +}) ``` diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index f4bf8a37..7e58c9aa 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -12,7 +12,7 @@ This guide covers the **JavaScript/TypeScript SDK** implementation. For other la ## Prerequisites - [Node.js LTS](https://nodejs.org/en/about/previous-releases) -- Your OpenTDF platform running locally (from Getting Started guide) +- Your OpenTDF platform running locally (from the [Getting Started guide](/quickstart)) :::warning Platform Must Be Running Before you begin, **make sure your OpenTDF platform is running!** @@ -83,7 +83,8 @@ async function main() { const oidcOrigin = 'https://keycloak.opentdf.local:9443/auth/realms/opentdf'; console.log(`📡 Connecting to platform: ${platformUrl}`); - // Create a token provider that handles caching and auto-refresh + // For this quickstart we use client credentials (server-side only). + // In a browser app, use refreshTokenProvider() with your OIDC login flow instead. const tokenProvider = clientCredentialsTokenProvider({ clientId: 'opentdf', clientSecret: 'secret', @@ -189,21 +190,22 @@ For additional policy management examples including managing attributes, namespa Let's work with the "department" attribute and add a "marketing" value. If you completed the [Quickstart setup guide](/quickstart), the attribute may already exist with finance and engineering values. If not, we'll create it: ```javascript -import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, attributeExists, attributeValueExists, OpenTDF } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; import { AttributeRuleTypeEnum } from '@opentdf/sdk/platform/policy/objects_pb.js'; const platformUrl = 'https://platform.opentdf.local:8443'; const oidcOrigin = 'https://keycloak.opentdf.local:9443/auth/realms/opentdf'; -// Create the OpenTDF and Platform clients with auth interceptor +// Client credentials for this quickstart only — use refreshTokenProvider() in browser apps const interceptors = [authTokenInterceptor(clientCredentialsTokenProvider({ clientId: 'opentdf', clientSecret: 'secret', oidcOrigin, }))]; -const client = new OpenTDF({ interceptors, platformUrl }); -const platform = new PlatformClient({ interceptors, platformUrl }); +const auth = { interceptors }; +const client = new OpenTDF({ ...auth, platformUrl }); +const platform = new PlatformClient({ ...auth, platformUrl }); // First, ensure the namespace exists let namespaceId; @@ -226,46 +228,36 @@ try { } } -// Get or create the department attribute -const listResp = await platform.v1.attributes.listAttributes({}); -let attribute = listResp.attributes.find( - (attr) => attr.name === 'department' && attr.namespace?.id === namespaceId -); - -if (!attribute) { - const attrResp = await platform.v1.attributes.createAttribute({ +// Create the department attribute if it doesn't exist +const deptFqn = 'https://opentdf.io/attr/department'; +if (!(await attributeExists(platformUrl, auth, deptFqn))) { + await platform.v1.attributes.createAttribute({ namespaceId, name: 'department', rule: AttributeRuleTypeEnum.ANY_OF, values: ['marketing'], }); - attribute = attrResp.attribute; - console.log(`✅ Created attribute: ${attribute?.name}`); + console.log('✅ Created department attribute with marketing value'); } else { - console.log(`✅ Found existing attribute: ${attribute.name}`); + console.log('✅ Using existing department attribute'); } -// Check if "marketing" value already exists -const targetValue = 'marketing'; -const valueExists = attribute?.values?.some((v) => v.value === targetValue); - -if (!valueExists) { +// Ensure the marketing value exists on the attribute +const marketingFqn = `${deptFqn}/value/marketing`; +if (!(await attributeValueExists(platformUrl, auth, marketingFqn))) { + // Need the attribute ID to add a value — fetch it by FQN + const listResp = await platform.v1.attributes.listAttributes({}); + const attribute = listResp.attributes.find( + (attr) => attr.name === 'department' && attr.namespace?.id === namespaceId + ); await platform.v1.attributes.createAttributeValue({ attributeId: attribute?.id, - value: targetValue, + value: 'marketing', }); - console.log(`✅ Added '${targetValue}' value to department attribute`); -} else { - console.log(`✅ Attribute 'department' already has '${targetValue}' value`); + console.log('✅ Added marketing value to department attribute'); } -console.log(`Full attribute FQN: https://opentdf.io/attr/department/value/${targetValue}`); - -// Re-fetch the attribute to get updated values with IDs -const refreshedList = await platform.v1.attributes.listAttributes({}); -attribute = refreshedList.attributes.find( - (attr) => attr.name === 'department' && attr.namespace?.id === namespaceId -); +console.log(`Full attribute FQN: ${marketingFqn}`); ``` :::warning @@ -310,25 +302,20 @@ import { const marketingValue = attribute?.values?.find((v) => v.value === targetValue); const attributeValueId = marketingValue?.id; -// Create a subject condition set that matches your identity +// Subject condition sets use a nested structure: sets → groups → conditions. +// This one matches any entity whose .clientId claim includes 'opentdf'. const scsResp = await platform.v1.subjectMapping.createSubjectConditionSet({ subjectConditionSet: { - subjectSets: [ - { - conditionGroups: [ - { - booleanOperator: ConditionBooleanTypeEnum.AND, - conditions: [ - { - subjectExternalSelectorValue: '.clientId', - operator: SubjectMappingOperatorEnum.IN, - subjectExternalValues: ['opentdf'], - }, - ], - }, - ], - }, - ], + subjectSets: [{ + conditionGroups: [{ + booleanOperator: ConditionBooleanTypeEnum.AND, + conditions: [{ + subjectExternalSelectorValue: '.clientId', + operator: SubjectMappingOperatorEnum.IN, + subjectExternalValues: ['opentdf'], + }], + }], + }], }, }); @@ -400,7 +387,7 @@ For reference, here's a complete example showing all the pieces together: ```javascript title="index.mjs" import fs from 'node:fs'; -import { authTokenInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; +import { authTokenInterceptor, clientCredentialsTokenProvider, attributeExists, attributeValueExists, OpenTDF } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; import { AttributeRuleTypeEnum, @@ -412,14 +399,15 @@ async function main() { const platformUrl = 'https://platform.opentdf.local:8443'; const oidcOrigin = 'https://keycloak.opentdf.local:9443/auth/realms/opentdf'; - // Create the OpenTDF and Platform clients with auth interceptor + // Client credentials for this quickstart only — use refreshTokenProvider() in browser apps const interceptors = [authTokenInterceptor(clientCredentialsTokenProvider({ clientId: 'opentdf', clientSecret: 'secret', oidcOrigin, }))]; - const client = new OpenTDF({ interceptors, platformUrl }); - const platform = new PlatformClient({ interceptors, platformUrl }); + const auth = { interceptors }; + const client = new OpenTDF({ ...auth, platformUrl }); + const platform = new PlatformClient({ ...auth, platformUrl }); // 1. Create namespace (or use existing) let namespaceId; @@ -431,7 +419,6 @@ async function main() { console.log(`✅ Created namespace: ${namespaceId}`); } catch (err) { if (err.message?.includes('already_exists')) { - // Namespace exists — fetch it directly by FQN const getNsResp = await platform.v1.namespace.getNamespace({ identifier: { case: 'fqn', value: 'https://opentdf.io' }, }); @@ -442,36 +429,32 @@ async function main() { } } - // 2. Create attribute with marketing value (or use existing) - let attribute; - try { - const attrResp = await platform.v1.attributes.createAttribute({ + // 2. Create the department attribute if it doesn't exist + const deptFqn = 'https://opentdf.io/attr/department'; + if (!(await attributeExists(platformUrl, auth, deptFqn))) { + await platform.v1.attributes.createAttribute({ namespaceId, name: 'department', rule: AttributeRuleTypeEnum.ANY_OF, values: ['marketing'], }); - attribute = attrResp.attribute; - console.log(`✅ Created attribute: ${attribute?.name}`); - } catch (err) { - if (err.message?.includes('already_exists')) { - const listResp = await platform.v1.attributes.listAttributes({}); - attribute = listResp.attributes.find( - (attr) => attr.name === 'department' && attr.namespace?.id === namespaceId - ); - console.log(`✅ Using existing attribute: ${attribute?.name}`); - - // Ensure the 'marketing' value exists on the attribute - if (attribute && !attribute.values?.some((v) => v.value === 'marketing')) { - await platform.v1.attributes.createAttributeValue({ - attributeId: attribute.id, - value: 'marketing', - }); - console.log(`✅ Added 'marketing' value to department attribute`); - } - } else { - throw err; - } + console.log('✅ Created department attribute with marketing value'); + } else { + console.log('✅ Using existing department attribute'); + } + + // Ensure the marketing value exists on the attribute + const marketingFqn = `${deptFqn}/value/marketing`; + if (!(await attributeValueExists(platformUrl, auth, marketingFqn))) { + const listResp = await platform.v1.attributes.listAttributes({}); + const attribute = listResp.attributes.find( + (attr) => attr.name === 'department' && attr.namespace?.id === namespaceId + ); + await platform.v1.attributes.createAttributeValue({ + attributeId: attribute?.id, + value: 'marketing', + }); + console.log('✅ Added marketing value to department attribute'); } // 3. Encrypt data with the marketing attribute @@ -494,35 +477,30 @@ async function main() { console.log('✅ TDF saved to encrypted.tdf'); // 5. Grant yourself access to the marketing attribute - // Re-fetch to get value IDs - const refreshedList = await platform.v1.attributes.listAttributes({}); - attribute = refreshedList.attributes.find( + // Fetch the attribute to get the marketing value ID for the subject mapping + const attrList = await platform.v1.attributes.listAttributes({}); + const deptAttr = attrList.attributes.find( (attr) => attr.name === 'department' && attr.namespace?.id === namespaceId ); - - const marketingValue = attribute?.values?.find((v) => v.value === 'marketing'); + const marketingValue = deptAttr?.values?.find((v) => v.value === 'marketing'); if (!marketingValue?.id) { throw new Error('Marketing value not found in department attribute'); } + // Subject condition sets use a nested structure: sets → groups → conditions. + // This one matches any entity whose .clientId claim includes 'opentdf'. const scsResp = await platform.v1.subjectMapping.createSubjectConditionSet({ subjectConditionSet: { - subjectSets: [ - { - conditionGroups: [ - { - booleanOperator: ConditionBooleanTypeEnum.AND, - conditions: [ - { - subjectExternalSelectorValue: '.clientId', - operator: SubjectMappingOperatorEnum.IN, - subjectExternalValues: ['opentdf'], - }, - ], - }, - ], - }, - ], + subjectSets: [{ + conditionGroups: [{ + booleanOperator: ConditionBooleanTypeEnum.AND, + conditions: [{ + subjectExternalSelectorValue: '.clientId', + operator: SubjectMappingOperatorEnum.IN, + subjectExternalValues: ['opentdf'], + }], + }], + }], }, }); diff --git a/docs/sdks/tdf.mdx b/docs/sdks/tdf.mdx index d09d244c..1091fcea 100644 --- a/docs/sdks/tdf.mdx +++ b/docs/sdks/tdf.mdx @@ -187,11 +187,33 @@ async createTDF(options: CreateTDFOptions): Promise **Parameters** -| Parameter | Required | Description | -|-----------|----------|-------------| -| Output destination | Required | Where the encrypted TDF bytes are written. | -| Plaintext source | Required | The data to encrypt. See the signature tab for your language for the expected type. | -| Configuration | Required* | Encryption options. A KAS endpoint must be specified unless autoconfigure is enabled. See [Encrypt Options](#encrypt-options). | + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `writer` | `io.Writer` | Where the encrypted TDF bytes are written. | +| `reader` | `io.ReadSeeker` | The plaintext data to encrypt. | +| `opts` | `...TDFOption` | Encryption options. At minimum, a KAS endpoint must be specified (e.g., `sdk.WithKasInformation()`). See [Encrypt Options](#encrypt-options). | + + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `plaintext` | `InputStream` | The plaintext data to encrypt. | +| `out` | `OutputStream` | Where the encrypted TDF bytes are written. | +| `config` | `Config.TDFConfig` | Encryption options. At minimum, a KAS endpoint must be specified. See [Encrypt Options](#encrypt-options). | + + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `options` | `CreateTDFOptions` | A single options object containing the source, KAS endpoint, attributes, and other settings. See [Encrypt Options](#encrypt-options). | + + + **Returns** diff --git a/docs/sdks/troubleshooting.mdx b/docs/sdks/troubleshooting.mdx index 191534af..cdee13dc 100644 --- a/docs/sdks/troubleshooting.mdx +++ b/docs/sdks/troubleshooting.mdx @@ -360,7 +360,12 @@ If you can't resolve an issue using this guide, here are the next steps: 2. **Search existing GitHub Discussions** — many common questions are already answered in the [opentdf/platform discussions](https://github.com/opentdf/platform/discussions). -3. **Open a GitHub issue** with: +3. **Open a GitHub issue** on the relevant repo: + - [opentdf/platform](https://github.com/opentdf/platform/issues) — Go SDK and platform + - [opentdf/java-sdk](https://github.com/opentdf/java-sdk/issues) — Java SDK + - [opentdf/web-sdk](https://github.com/opentdf/web-sdk/issues) — JavaScript/TypeScript SDK + + Include: - SDK language and version - Full error message (including stack trace if available) - Minimal code that reproduces the issue From 5b19bde4bc6ea7daa312a772c7de7628531a9a4b Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Mon, 6 Apr 2026 12:46:22 -0700 Subject: [PATCH 11/12] fix(docs): split LoadTDF parameters into per-SDK tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same treatment as CreateTDF — each SDK gets its own parameter table matching its actual signature instead of a vague shared table. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/tdf.mdx | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/sdks/tdf.mdx b/docs/sdks/tdf.mdx index 1091fcea..adb45075 100644 --- a/docs/sdks/tdf.mdx +++ b/docs/sdks/tdf.mdx @@ -359,10 +359,31 @@ open(options: ReadOptions): TDFReader **Parameters** -| Parameter | Required | Description | -|-----------|----------|-------------| -| Encrypted TDF source | Required | The TDF to open. See the signature tab for your language for the expected type. For JavaScript, `source` accepts a `buffer` (`Uint8Array`) or `stream` (`ReadableStream`). | -| Configuration | Optional | Decryption options. Defaults allow any KAS endpoint. See [Decrypt Options](#decrypt-options). | + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `reader` | `io.ReadSeeker` | The encrypted TDF to open. | +| `opts` | `...TDFReaderOption` | Optional decryption settings. See [Decrypt Options](#decrypt-options). | + + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `channel` | `SeekableByteChannel` | The encrypted TDF to open. | +| `config` | `Config.TDFReaderConfig` | Decryption settings. Use `Config.newTDFReaderConfig()` for defaults. See [Decrypt Options](#decrypt-options). | + + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `options` | `ReadOptions` | A single options object. `source` accepts a `buffer` (`Uint8Array`) or `stream` (`ReadableStream`). See [Decrypt Options](#decrypt-options). | + + + **Returns** From d2afc597bdd3b1c4d2c91a83ee905c7798d6abbf Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Mon, 6 Apr 2026 13:59:40 -0700 Subject: [PATCH 12/12] fix(docs): address review feedback on auth patterns and quickstart bug - Add Access Token section to authentication.mdx for apps that only have an access token (no refresh token available) - Update intro tip and auth decision guide to present access token as a valid browser option alongside refresh token - Fix DPoP JS example to use generic token provider instead of clientCredentialsTokenProvider - Fix quickstart scoping bug: move attribute lookup outside if block so it's available for subject mapping section - Fix undefined targetValue reference in quickstart Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/guides/authentication-guide.mdx | 10 +++++--- docs/sdks/authentication.mdx | 38 +++++++++++++++++++++++----- docs/sdks/quickstart/javascript.mdx | 12 ++++----- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/docs/guides/authentication-guide.mdx b/docs/guides/authentication-guide.mdx index 3ab87261..a9331d25 100644 --- a/docs/guides/authentication-guide.mdx +++ b/docs/guides/authentication-guide.mdx @@ -62,7 +62,8 @@ flowchart TD | Scenario | Recommended Method | Notes | |----------|-------------------|-------| | Backend service / microservice | [Client Credentials](/sdks/authentication#client-credentials) | Most straightforward. Use `clientCredentialsTokenProvider()` — it caches tokens and auto-refreshes on expiry. | -| Web application (browser) | [OIDC Login Flow](/sdks/authentication#refresh-token) | JS SDK only. Use `refreshTokenProvider()` with a refresh token from your OIDC login flow. Never expose client secrets in browser code. | +| Web application (browser) with refresh token | [Refresh Token](/sdks/authentication#refresh-token) | JS SDK only. Use `refreshTokenProvider()` with a refresh token from your OIDC login flow. Handles automatic renewal. | +| Web application (browser) with access token only | [Access Token](/sdks/authentication#access-token) | JS SDK only. Use `authTokenInterceptor()` with a function that returns your access token. Use when your IdP doesn't issue refresh tokens. | | CLI tool (interactive user) | `otdfctl auth login` | Opens a browser for OIDC login. See [CLI auth docs](/components/cli/auth/login). | | CI/CD pipeline / automated job | [Client Credentials](/sdks/authentication#client-credentials) | Use a dedicated service account. Rotate secrets regularly. | | Federated identity / SAML | [Token Exchange](/sdks/authentication#token-exchange) | Exchange an existing token for one the platform accepts. | @@ -81,11 +82,12 @@ See [SDK code examples](/sdks/authentication#client-credentials) for Go, Java, a ### OIDC Login Flow (Browser Apps) -For browser-based applications, your app completes an [OIDC Authorization Code flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) to obtain an access token and (optionally) a refresh token from the IdP. The IdP returns both tokens after a successful `/token` call with a valid authorization code — though the refresh token is only issued if the IdP is configured to provide one. You then pass the refresh token to `refreshTokenProvider()`, which handles token exchange and automatic refresh. +For browser-based applications, your app completes an [OIDC Authorization Code flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) to obtain an access token and (optionally) a refresh token from the IdP. -**What you need:** A refresh token from a completed OIDC login flow. +- **If you have a refresh token**, use [`refreshTokenProvider()`](/sdks/authentication#refresh-token) — it handles token exchange and automatic renewal. +- **If you only have an access token** (because your IdP doesn't issue refresh tokens or the grant is disabled for security reasons), use [`authTokenInterceptor()`](/sdks/authentication#access-token) with a function that returns your token. -See [SDK code examples](/sdks/authentication#refresh-token) (JavaScript only). +See SDK code examples: [Refresh Token](/sdks/authentication#refresh-token) | [Access Token](/sdks/authentication#access-token) (JavaScript only). ### Token Exchange (Federated Identity) diff --git a/docs/sdks/authentication.mdx b/docs/sdks/authentication.mdx index c941aaf4..0fa58b76 100644 --- a/docs/sdks/authentication.mdx +++ b/docs/sdks/authentication.mdx @@ -11,7 +11,7 @@ import TabItem from '@theme/TabItem'; The OpenTDF SDKs authenticate with an [OIDC](https://openid.net/developers/how-connect-works/)-compatible identity provider (IdP) to obtain access tokens for the platform. The platform itself is a **resource server**, not an identity provider — you bring your own IdP (Keycloak is the reference implementation). :::tip Not sure which method to use? -The JavaScript SDK is designed for **browser applications**. For browser apps, start with [Refresh Token](#refresh-token) (the most common browser pattern). For backend scripts and testing, see [Client Credentials](#client-credentials). See the [Authentication Decision Guide](/guides/authentication-guide) for a full comparison. +The JavaScript SDK is designed for **browser applications**. If your app already has an access token, use [Access Token](#access-token). If your OIDC flow provides a refresh token, use [Refresh Token](#refresh-token) for automatic renewal. For backend scripts and testing, see [Client Credentials](#client-credentials). See the [Authentication Decision Guide](/guides/authentication-guide) for a full comparison. ::: ## Client Credentials @@ -144,6 +144,32 @@ const client = new OpenTDF({ }); ``` +## Access Token + +Use an **access token** when your application already has a valid token from its own authentication system — for example, from an OIDC library like `oidc-client-ts`, an auth context provider, or a cookie. This is common when the IdP doesn't issue refresh tokens or when your security policy restricts their use. + +:::info JavaScript SDK only +Access token authentication is currently available in the JavaScript SDK only. Go and Java SDKs handle token lifecycle differently — see [Client Credentials](#client-credentials) for backend use cases, or [Custom Token Source](#custom-token-source) for advanced integrations. +::: + +```typescript +import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk'; + +// Pass any () => Promise function that returns a valid access token. +// Example: read from your OIDC library's user manager. +const client = new OpenTDF({ + interceptors: [authTokenInterceptor(async () => { + const user = await userManager.getUser(); + return user?.access_token ?? ''; + })], + platformUrl: 'http://localhost:8080', +}); +``` + +:::warning You manage token lifecycle +Unlike the built-in token providers (`clientCredentialsTokenProvider`, `refreshTokenProvider`), a custom function does not automatically cache or refresh tokens. If your tokens are short-lived, ensure your function returns a fresh token when called — `authTokenInterceptor` calls it on every request. +::: + ## Certificate Exchange (mTLS) Use **certificate-based authentication** in environments that require [mutual TLS (mTLS)](https://www.cloudflare.com/learning/access-management/what-is-mutual-tls/) with client certificates. @@ -326,14 +352,12 @@ SDK sdk = new SDKBuilder() DPoP is off by default with interceptors. Use `authTokenDPoPInterceptor()` to enable it: ```typescript -import { authTokenDPoPInterceptor, clientCredentialsTokenProvider, OpenTDF } from '@opentdf/sdk'; +import { authTokenDPoPInterceptor, OpenTDF } from '@opentdf/sdk'; +// Use any TokenProvider: refreshTokenProvider(), clientCredentialsTokenProvider(), +// externalJwtTokenProvider(), or a custom () => Promise function. const dpopInterceptor = authTokenDPoPInterceptor({ - tokenProvider: clientCredentialsTokenProvider({ - clientId: 'my-client-id', - clientSecret: 'my-client-secret', - oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', - }), + tokenProvider: getAccessToken, // your token provider here }); const client = new OpenTDF({ diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index 7e58c9aa..1643e304 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -244,12 +244,12 @@ if (!(await attributeExists(platformUrl, auth, deptFqn))) { // Ensure the marketing value exists on the attribute const marketingFqn = `${deptFqn}/value/marketing`; +const listResp = await platform.v1.attributes.listAttributes({}); +const attribute = listResp.attributes.find( + (attr) => attr.name === 'department' && attr.namespace?.id === namespaceId +); + if (!(await attributeValueExists(platformUrl, auth, marketingFqn))) { - // Need the attribute ID to add a value — fetch it by FQN - const listResp = await platform.v1.attributes.listAttributes({}); - const attribute = listResp.attributes.find( - (attr) => attr.name === 'department' && attr.namespace?.id === namespaceId - ); await platform.v1.attributes.createAttributeValue({ attributeId: attribute?.id, value: 'marketing', @@ -299,7 +299,7 @@ import { } from '@opentdf/sdk/platform/policy/objects_pb.js'; // Get the attribute value ID for "marketing" -const marketingValue = attribute?.values?.find((v) => v.value === targetValue); +const marketingValue = attribute?.values?.find((v) => v.value === 'marketing'); const attributeValueId = marketingValue?.id; // Subject condition sets use a nested structure: sets → groups → conditions.