@opentdf/sdk 0.13.0-beta.122 → 0.13.0-beta.123
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -10
- package/dist/cjs/src/access/access-rpc.js +6 -5
- package/dist/cjs/src/access.js +18 -5
- package/dist/cjs/src/auth/interceptors.js +186 -0
- package/dist/cjs/src/index.js +6 -2
- package/dist/cjs/src/opentdf.js +40 -32
- package/dist/cjs/src/platform.js +3 -46
- package/dist/cjs/src/policy/api.js +9 -5
- package/dist/cjs/src/policy/discovery.js +10 -9
- package/dist/cjs/tdf3/src/client/index.js +35 -17
- package/dist/cjs/tdf3/src/tdf.js +8 -7
- package/dist/types/src/access/access-rpc.d.ts +3 -3
- package/dist/types/src/access/access-rpc.d.ts.map +1 -1
- package/dist/types/src/access.d.ts +3 -3
- package/dist/types/src/access.d.ts.map +1 -1
- package/dist/types/src/auth/interceptors.d.ts +99 -0
- package/dist/types/src/auth/interceptors.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/opentdf.d.ts +18 -15
- package/dist/types/src/opentdf.d.ts.map +1 -1
- package/dist/types/src/platform.d.ts +6 -3
- package/dist/types/src/platform.d.ts.map +1 -1
- package/dist/types/src/policy/api.d.ts +3 -3
- package/dist/types/src/policy/api.d.ts.map +1 -1
- package/dist/types/src/policy/discovery.d.ts +5 -5
- package/dist/types/src/policy/discovery.d.ts.map +1 -1
- package/dist/types/tdf3/src/client/index.d.ts +10 -1
- package/dist/types/tdf3/src/client/index.d.ts.map +1 -1
- package/dist/types/tdf3/src/tdf.d.ts +5 -2
- package/dist/types/tdf3/src/tdf.d.ts.map +1 -1
- package/dist/web/src/access/access-rpc.js +6 -5
- package/dist/web/src/access.js +18 -5
- package/dist/web/src/auth/interceptors.js +142 -0
- package/dist/web/src/index.js +2 -1
- package/dist/web/src/opentdf.js +40 -32
- package/dist/web/src/platform.js +3 -46
- package/dist/web/src/policy/api.js +9 -5
- package/dist/web/src/policy/discovery.js +10 -9
- package/dist/web/tdf3/src/client/index.js +35 -17
- package/dist/web/tdf3/src/tdf.js +8 -7
- package/package.json +1 -1
- package/src/access/access-rpc.ts +5 -5
- package/src/access.ts +29 -13
- package/src/auth/interceptors.ts +197 -0
- package/src/index.ts +10 -0
- package/src/opentdf.ts +54 -34
- package/src/platform.ts +8 -52
- package/src/policy/api.ts +8 -5
- package/src/policy/discovery.ts +9 -9
- package/tdf3/src/client/index.ts +46 -17
- package/tdf3/src/tdf.ts +14 -11
package/src/opentdf.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AuthProvider } from './auth/providers.js';
|
|
2
|
+
import { type Interceptor } from '@connectrpc/connect';
|
|
2
3
|
import { ConfigurationError, InvalidFileError } from './errors.js';
|
|
3
4
|
export { Client as TDF3Client } from '../tdf3/src/client/index.js';
|
|
4
5
|
import { Chunker, fromSource, sourceToStream, type Source } from './seekable.js';
|
|
@@ -164,8 +165,17 @@ export type OpenTDFOptions = {
|
|
|
164
165
|
/** Platform URL. */
|
|
165
166
|
platformUrl?: string;
|
|
166
167
|
|
|
167
|
-
/**
|
|
168
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Connect RPC interceptors for authentication. Preferred over authProvider.
|
|
170
|
+
* Use `authTokenInterceptor()` or `authTokenDPoPInterceptor()` to create interceptors.
|
|
171
|
+
*/
|
|
172
|
+
interceptors?: Interceptor[];
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Auth provider for connections to the policy service and KASes.
|
|
176
|
+
* @deprecated since 0.14.0. Use `interceptors` with `authTokenInterceptor()` or `authTokenDPoPInterceptor()` instead.
|
|
177
|
+
*/
|
|
178
|
+
authProvider?: AuthProvider;
|
|
169
179
|
|
|
170
180
|
/** Default settings for 'encrypt' type requests. */
|
|
171
181
|
defaultCreateOptions?: Omit<CreateOptions, 'source'>;
|
|
@@ -236,18 +246,10 @@ export type TDFReader = {
|
|
|
236
246
|
* It also requires a platform URL to be set, which is used to fetch key access servers and policies.
|
|
237
247
|
* @example
|
|
238
248
|
* ```
|
|
239
|
-
* import {
|
|
240
|
-
*
|
|
241
|
-
* const oidcCredentials: RefreshTokenCredentials = {
|
|
242
|
-
* clientId: keycloakClientId,
|
|
243
|
-
* exchange: 'refresh',
|
|
244
|
-
* refreshToken: refreshToken,
|
|
245
|
-
* oidcOrigin: keycloakUrl,
|
|
246
|
-
* };
|
|
247
|
-
* const authProvider = await AuthProviders.refreshAuthProvider(oidcCredentials);
|
|
249
|
+
* import { authTokenInterceptor, OpenTDF } from '@opentdf/sdk';
|
|
248
250
|
*
|
|
249
251
|
* const client = new OpenTDF({
|
|
250
|
-
*
|
|
252
|
+
* interceptors: [authTokenInterceptor(() => `${myAuth.token.accessToken}`)],
|
|
251
253
|
* platformUrl: 'https://platform.example.com',
|
|
252
254
|
* });
|
|
253
255
|
*
|
|
@@ -264,8 +266,10 @@ export class OpenTDF {
|
|
|
264
266
|
readonly platformUrl: string;
|
|
265
267
|
/** The policy service endpoint */
|
|
266
268
|
readonly policyEndpoint: string;
|
|
267
|
-
/** The auth provider for the OpenTDF instance. */
|
|
268
|
-
readonly authProvider
|
|
269
|
+
/** The auth provider for the OpenTDF instance (deprecated, use interceptors). */
|
|
270
|
+
readonly authProvider?: AuthProvider;
|
|
271
|
+
/** Connect RPC interceptors for authentication. */
|
|
272
|
+
readonly interceptors?: Interceptor[];
|
|
269
273
|
/** If DPoP is enabled for this instance. */
|
|
270
274
|
readonly dpopEnabled: boolean;
|
|
271
275
|
/** Default options for creating TDF objects. */
|
|
@@ -283,6 +287,7 @@ export class OpenTDF {
|
|
|
283
287
|
|
|
284
288
|
constructor({
|
|
285
289
|
authProvider,
|
|
290
|
+
interceptors,
|
|
286
291
|
dpopKeys,
|
|
287
292
|
defaultCreateOptions,
|
|
288
293
|
defaultReadOptions,
|
|
@@ -291,7 +296,11 @@ export class OpenTDF {
|
|
|
291
296
|
platformUrl,
|
|
292
297
|
cryptoService,
|
|
293
298
|
}: OpenTDFOptions) {
|
|
299
|
+
if (!authProvider && !interceptors?.length) {
|
|
300
|
+
throw new ConfigurationError('Either authProvider or interceptors must be provided.');
|
|
301
|
+
}
|
|
294
302
|
this.authProvider = authProvider;
|
|
303
|
+
this.interceptors = interceptors;
|
|
295
304
|
this.defaultCreateOptions = defaultCreateOptions || {};
|
|
296
305
|
this.defaultReadOptions = defaultReadOptions || {};
|
|
297
306
|
this.dpopEnabled = !disableDPoP;
|
|
@@ -308,6 +317,7 @@ export class OpenTDF {
|
|
|
308
317
|
this.dpopKeys = dpopKeys ?? this.cryptoService.generateSigningKeyPair();
|
|
309
318
|
this.tdf3Client = new TDF3Client({
|
|
310
319
|
authProvider,
|
|
320
|
+
interceptors,
|
|
311
321
|
dpopEnabled: this.dpopEnabled,
|
|
312
322
|
dpopKeys: this.dpopEnabled ? this.dpopKeys : undefined,
|
|
313
323
|
kasEndpoint: this.platformUrl || 'https://disallow.all.invalid',
|
|
@@ -315,21 +325,31 @@ export class OpenTDF {
|
|
|
315
325
|
policyEndpoint,
|
|
316
326
|
cryptoService: this.cryptoService,
|
|
317
327
|
});
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
328
|
+
|
|
329
|
+
if (interceptors?.length && !authProvider) {
|
|
330
|
+
// Interceptor path: no updateClientPublicKey needed.
|
|
331
|
+
// DPoP key binding is handled by the interceptor itself.
|
|
332
|
+
this.ready = Promise.resolve();
|
|
333
|
+
} else if (authProvider) {
|
|
334
|
+
// Legacy AuthProvider path: eagerly bind DPoP keys to the auth provider
|
|
335
|
+
// so PlatformClient can make gRPC calls without waiting for a TDF
|
|
336
|
+
// operation first.
|
|
337
|
+
// Note: TDF3Client.createSessionKeys() also calls updateClientPublicKey
|
|
338
|
+
// with the same keys, but the duplicate call is benign —
|
|
339
|
+
// refreshTokenClaimsWithClientPubkeyIfNeeded short-circuits when
|
|
340
|
+
// the signing key hasn't changed.
|
|
341
|
+
this.ready = this.dpopEnabled
|
|
342
|
+
? this.dpopKeys.then((keys) => authProvider.updateClientPublicKey(keys))
|
|
343
|
+
: Promise.resolve();
|
|
344
|
+
// Prevent unhandled rejection if caller doesn't await ready.
|
|
345
|
+
// The error will still surface via TDF3Client's own key binding
|
|
346
|
+
// when encrypt/decrypt is called.
|
|
347
|
+
this.ready.catch((err) => {
|
|
348
|
+
console.warn('OpenTDF: DPoP key binding failed during initialization:', err);
|
|
349
|
+
});
|
|
350
|
+
} else {
|
|
351
|
+
this.ready = Promise.resolve();
|
|
352
|
+
}
|
|
333
353
|
}
|
|
334
354
|
|
|
335
355
|
/** Creates a new TDF stream. */
|
|
@@ -485,9 +505,9 @@ class ZTDFReader {
|
|
|
485
505
|
|
|
486
506
|
const dpopKeys = await this.client.dpopKeys;
|
|
487
507
|
|
|
488
|
-
const {
|
|
489
|
-
if (!
|
|
490
|
-
throw new ConfigurationError('authProvider
|
|
508
|
+
const { auth, cryptoService } = this.client;
|
|
509
|
+
if (!auth) {
|
|
510
|
+
throw new ConfigurationError('authProvider or interceptors are required');
|
|
491
511
|
}
|
|
492
512
|
|
|
493
513
|
let allowList: OriginAllowList | undefined;
|
|
@@ -498,14 +518,14 @@ class ZTDFReader {
|
|
|
498
518
|
this.opts.ignoreAllowlist
|
|
499
519
|
);
|
|
500
520
|
} else if (this.opts.platformUrl) {
|
|
501
|
-
allowList = await fetchKeyAccessServers(this.opts.platformUrl,
|
|
521
|
+
allowList = await fetchKeyAccessServers(this.opts.platformUrl, auth);
|
|
502
522
|
}
|
|
503
523
|
|
|
504
524
|
const overview = await this.overview;
|
|
505
525
|
const oldStream = await decryptStreamFrom(
|
|
506
526
|
{
|
|
507
527
|
allowList,
|
|
508
|
-
|
|
528
|
+
auth,
|
|
509
529
|
chunker: this.source,
|
|
510
530
|
concurrencyLimit: 1,
|
|
511
531
|
cryptoService,
|
package/src/platform.ts
CHANGED
|
@@ -3,7 +3,8 @@ export * as platformConnectWeb from '@connectrpc/connect-web';
|
|
|
3
3
|
export * as platformConnect from '@connectrpc/connect';
|
|
4
4
|
|
|
5
5
|
import { createConnectTransport } from '@connectrpc/connect-web';
|
|
6
|
-
import { AuthProvider } from '../tdf3/index.js';
|
|
6
|
+
import type { AuthProvider } from '../tdf3/index.js';
|
|
7
|
+
import { authProviderInterceptor } from './auth/interceptors.js';
|
|
7
8
|
|
|
8
9
|
import { Client, createClient, Interceptor } from '@connectrpc/connect';
|
|
9
10
|
import { WellKnownService } from './platform/wellknownconfiguration/wellknown_configuration_pb.js';
|
|
@@ -44,9 +45,12 @@ export interface PlatformServicesV2 {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
export interface PlatformClientOptions {
|
|
47
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Authentication provider for generating auth interceptor.
|
|
50
|
+
* @deprecated since 0.14.0. Use `interceptors` with `authTokenInterceptor()` or `authTokenDPoPInterceptor()` instead.
|
|
51
|
+
*/
|
|
48
52
|
authProvider?: AuthProvider;
|
|
49
|
-
/** Array of
|
|
53
|
+
/** Array of interceptors to apply to rpc requests. Preferred over authProvider. */
|
|
50
54
|
interceptors?: Interceptor[];
|
|
51
55
|
/** Base URL of the platform API. */
|
|
52
56
|
platformUrl: string;
|
|
@@ -85,8 +89,7 @@ export class PlatformClient {
|
|
|
85
89
|
const interceptors: Interceptor[] = [];
|
|
86
90
|
|
|
87
91
|
if (options.authProvider) {
|
|
88
|
-
|
|
89
|
-
interceptors.push(authInterceptor);
|
|
92
|
+
interceptors.push(authProviderInterceptor(options.authProvider));
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
if (options.interceptors?.length) {
|
|
@@ -120,50 +123,3 @@ export class PlatformClient {
|
|
|
120
123
|
};
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Creates an interceptor that adds authentication headers to outgoing requests.
|
|
126
|
-
*
|
|
127
|
-
* This function uses the provided `AuthProvider` to generate authentication credentials
|
|
128
|
-
* for each request. The `AuthProvider` is expected to implement a `withCreds` method
|
|
129
|
-
* that returns an object containing authentication headers. These headers are then
|
|
130
|
-
* added to the request before it is sent to the server.
|
|
131
|
-
*
|
|
132
|
-
*/
|
|
133
|
-
function createAuthInterceptor(authProvider: AuthProvider): Interceptor {
|
|
134
|
-
const authInterceptor: Interceptor = (next) => async (req) => {
|
|
135
|
-
const url = new URL(req.url);
|
|
136
|
-
const pathOnly = url.pathname;
|
|
137
|
-
// Signs only the path of the url in the request
|
|
138
|
-
let token;
|
|
139
|
-
try {
|
|
140
|
-
token = await authProvider.withCreds({
|
|
141
|
-
url: pathOnly,
|
|
142
|
-
method: 'POST',
|
|
143
|
-
// Start with any headers Connect already has
|
|
144
|
-
headers: {
|
|
145
|
-
...Object.fromEntries(req.header.entries()),
|
|
146
|
-
'Content-Type': 'application/json',
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
} catch (err) {
|
|
150
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
151
|
-
if (msg.includes('public key') || msg.includes('updateClientPublicKey')) {
|
|
152
|
-
throw new Error(
|
|
153
|
-
'PlatformClient: DPoP key binding is not complete. ' +
|
|
154
|
-
'If you are using OpenTDF with PlatformClient, create OpenTDF first and ' +
|
|
155
|
-
'`await client.ready` before constructing PlatformClient. ' +
|
|
156
|
-
`Original error: ${msg}`
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
throw err;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
Object.entries(token.headers).forEach(([key, value]) => {
|
|
163
|
-
req.header.set(key, value);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
return await next(req);
|
|
167
|
-
};
|
|
168
|
-
return authInterceptor;
|
|
169
|
-
}
|
package/src/policy/api.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NetworkError } from '../errors.js';
|
|
2
|
-
import {
|
|
2
|
+
import { type AuthConfig, resolveInterceptors } from '../auth/interceptors.js';
|
|
3
3
|
import { extractRpcErrorMessage, getPlatformUrlFromKasEndpoint } from '../utils.js';
|
|
4
4
|
import { PlatformClient } from '../platform.js';
|
|
5
5
|
import { Value } from './attributes.js';
|
|
@@ -12,11 +12,11 @@ import { ValueSchema } from '../platform/policy/objects_pb.js';
|
|
|
12
12
|
// TODO KAS: go over web-sdk and remove policyEndpoint that is only defined to be used here
|
|
13
13
|
export async function attributeFQNsAsValues(
|
|
14
14
|
platformUrl: string,
|
|
15
|
-
|
|
15
|
+
auth: AuthConfig,
|
|
16
16
|
...fqns: string[]
|
|
17
17
|
): Promise<Value[]> {
|
|
18
18
|
platformUrl = getPlatformUrlFromKasEndpoint(platformUrl);
|
|
19
|
-
const platform = new PlatformClient({
|
|
19
|
+
const platform = new PlatformClient({ interceptors: resolveInterceptors(auth), platformUrl });
|
|
20
20
|
|
|
21
21
|
let response: GetAttributeValuesByFqnsResponse;
|
|
22
22
|
try {
|
|
@@ -52,7 +52,7 @@ export async function attributeFQNsAsValues(
|
|
|
52
52
|
// Get root certificates from a namespace
|
|
53
53
|
export async function getRootCertsFromNamespace(
|
|
54
54
|
platformUrl: string,
|
|
55
|
-
|
|
55
|
+
auth?: AuthConfig,
|
|
56
56
|
namespaceId?: string,
|
|
57
57
|
fqn?: string
|
|
58
58
|
): Promise<Certificate[]> {
|
|
@@ -63,7 +63,10 @@ export async function getRootCertsFromNamespace(
|
|
|
63
63
|
throw new Error('Either namespaceId or fqn must be provided');
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
const platform = new PlatformClient({
|
|
66
|
+
const platform = new PlatformClient({
|
|
67
|
+
...(auth ? { interceptors: resolveInterceptors(auth) } : {}),
|
|
68
|
+
platformUrl,
|
|
69
|
+
});
|
|
67
70
|
|
|
68
71
|
let response: GetNamespaceResponse;
|
|
69
72
|
try {
|
package/src/policy/discovery.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConnectError, Code } from '@connectrpc/connect';
|
|
2
2
|
import { AttributeNotFoundError, ConfigurationError, NetworkError } from '../errors.js';
|
|
3
|
-
import { type
|
|
3
|
+
import { type AuthConfig, resolveInterceptors } from '../auth/interceptors.js';
|
|
4
4
|
import { extractRpcErrorMessage, validateSecureUrl } from '../utils.js';
|
|
5
5
|
import { PlatformClient } from '../platform.js';
|
|
6
6
|
import type { Attribute } from '../platform/policy/objects_pb.js';
|
|
@@ -49,13 +49,13 @@ const ATTRIBUTE_FQN_RE = /^https?:\/\/[a-zA-Z0-9._~%-]+\/attr\/[a-zA-Z0-9._~%-]+
|
|
|
49
49
|
*/
|
|
50
50
|
export async function listAttributes(
|
|
51
51
|
platformUrl: string,
|
|
52
|
-
|
|
52
|
+
auth: AuthConfig,
|
|
53
53
|
namespace?: string
|
|
54
54
|
): Promise<Attribute[]> {
|
|
55
55
|
if (!validateSecureUrl(platformUrl)) {
|
|
56
56
|
throw new ConfigurationError('platformUrl must use HTTPS protocol');
|
|
57
57
|
}
|
|
58
|
-
const platform = new PlatformClient({
|
|
58
|
+
const platform = new PlatformClient({ interceptors: resolveInterceptors(auth), platformUrl });
|
|
59
59
|
const result: Attribute[] = [];
|
|
60
60
|
let nextOffset = 0;
|
|
61
61
|
|
|
@@ -106,7 +106,7 @@ export async function listAttributes(
|
|
|
106
106
|
*/
|
|
107
107
|
export async function validateAttributes(
|
|
108
108
|
platformUrl: string,
|
|
109
|
-
|
|
109
|
+
auth: AuthConfig,
|
|
110
110
|
fqns: string[]
|
|
111
111
|
): Promise<void> {
|
|
112
112
|
if (!fqns || fqns.length === 0) {
|
|
@@ -129,7 +129,7 @@ export async function validateAttributes(
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
const platform = new PlatformClient({
|
|
132
|
+
const platform = new PlatformClient({ interceptors: resolveInterceptors(auth), platformUrl });
|
|
133
133
|
let resp;
|
|
134
134
|
try {
|
|
135
135
|
resp = await platform.v1.attributes.getAttributeValuesByFqns({ fqns });
|
|
@@ -159,7 +159,7 @@ export async function validateAttributes(
|
|
|
159
159
|
*/
|
|
160
160
|
export async function attributeExists(
|
|
161
161
|
platformUrl: string,
|
|
162
|
-
|
|
162
|
+
auth: AuthConfig,
|
|
163
163
|
attributeFqn: string
|
|
164
164
|
): Promise<boolean> {
|
|
165
165
|
if (!validateSecureUrl(platformUrl)) {
|
|
@@ -170,7 +170,7 @@ export async function attributeExists(
|
|
|
170
170
|
throw new ConfigurationError('invalid attribute FQN format');
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
const platform = new PlatformClient({
|
|
173
|
+
const platform = new PlatformClient({ interceptors: resolveInterceptors(auth), platformUrl });
|
|
174
174
|
try {
|
|
175
175
|
await platform.v1.attributes.getAttribute({
|
|
176
176
|
identifier: { case: 'fqn', value: attributeFqn },
|
|
@@ -199,7 +199,7 @@ export async function attributeExists(
|
|
|
199
199
|
*/
|
|
200
200
|
export async function attributeValueExists(
|
|
201
201
|
platformUrl: string,
|
|
202
|
-
|
|
202
|
+
auth: AuthConfig,
|
|
203
203
|
valueFqn: string
|
|
204
204
|
): Promise<boolean> {
|
|
205
205
|
if (!validateSecureUrl(platformUrl)) {
|
|
@@ -210,7 +210,7 @@ export async function attributeValueExists(
|
|
|
210
210
|
throw new ConfigurationError('invalid attribute value FQN format');
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
const platform = new PlatformClient({
|
|
213
|
+
const platform = new PlatformClient({ interceptors: resolveInterceptors(auth), platformUrl });
|
|
214
214
|
let resp;
|
|
215
215
|
try {
|
|
216
216
|
resp = await platform.v1.attributes.getAttributeValuesByFqns({ fqns: [valueFqn] });
|
package/tdf3/src/client/index.ts
CHANGED
|
@@ -19,6 +19,8 @@ import { OIDCRefreshTokenProvider } from '../../../src/auth/oidc-refreshtoken-pr
|
|
|
19
19
|
import { OIDCExternalJwtProvider } from '../../../src/auth/oidc-externaljwt-provider.js';
|
|
20
20
|
import { CryptoService } from '../crypto/declarations.js';
|
|
21
21
|
import { type AuthProvider, HttpRequest, withHeaders } from '../../../src/auth/auth.js';
|
|
22
|
+
import { type AuthConfig } from '../../../src/auth/interceptors.js';
|
|
23
|
+
import { type Interceptor } from '@connectrpc/connect';
|
|
22
24
|
import { getPlatformUrlFromKasEndpoint, rstrip, validateSecureUrl } from '../../../src/utils.js';
|
|
23
25
|
|
|
24
26
|
import {
|
|
@@ -154,7 +156,10 @@ export interface ClientConfig {
|
|
|
154
156
|
kasPublicKey?: string;
|
|
155
157
|
oidcOrigin?: string;
|
|
156
158
|
externalJwt?: string;
|
|
159
|
+
/** @deprecated since 0.14.0. Use `interceptors` instead. */
|
|
157
160
|
authProvider?: AuthProvider;
|
|
161
|
+
/** Connect RPC interceptors for authentication. Preferred over authProvider. */
|
|
162
|
+
interceptors?: Interceptor[];
|
|
158
163
|
readerUrl?: string;
|
|
159
164
|
entityObjectEndpoint?: string;
|
|
160
165
|
fileStreamServiceWorker?: string;
|
|
@@ -347,7 +352,11 @@ export class Client {
|
|
|
347
352
|
|
|
348
353
|
readonly clientId?: string;
|
|
349
354
|
|
|
350
|
-
|
|
355
|
+
/**
|
|
356
|
+
* Resolved auth configuration. Set once in the constructor from either
|
|
357
|
+
* authProvider or interceptors. Threaded through all internal layers.
|
|
358
|
+
*/
|
|
359
|
+
readonly auth?: AuthConfig;
|
|
351
360
|
|
|
352
361
|
readonly readerUrl?: string;
|
|
353
362
|
|
|
@@ -424,13 +433,16 @@ export class Client {
|
|
|
424
433
|
this.easEndpoint = clientConfig.easEndpoint;
|
|
425
434
|
}
|
|
426
435
|
|
|
427
|
-
this.authProvider = config.authProvider;
|
|
428
436
|
this.clientConfig = clientConfig;
|
|
429
437
|
|
|
438
|
+
// Resolve auth once at the boundary. Internally, only `this.auth` is used.
|
|
439
|
+
let authProvider = config.authProvider;
|
|
430
440
|
this.clientId = clientConfig.clientId;
|
|
431
|
-
if (!
|
|
441
|
+
if (!authProvider && !config.interceptors?.length) {
|
|
432
442
|
if (!clientConfig.clientId) {
|
|
433
|
-
throw new ConfigurationError(
|
|
443
|
+
throw new ConfigurationError(
|
|
444
|
+
'Client ID, custom AuthProvider, or interceptors must be defined'
|
|
445
|
+
);
|
|
434
446
|
}
|
|
435
447
|
|
|
436
448
|
//Are we exchanging a refreshToken for a bearer token (normal AuthCode browser auth flow)?
|
|
@@ -438,7 +450,7 @@ export class Client {
|
|
|
438
450
|
//browser-based OIDC login and authentication process against the OIDC endpoint using their chosen method,
|
|
439
451
|
//and provide us with a valid refresh token/clientId obtained from that process.
|
|
440
452
|
if (clientConfig.refreshToken) {
|
|
441
|
-
|
|
453
|
+
authProvider = new OIDCRefreshTokenProvider(
|
|
442
454
|
{
|
|
443
455
|
clientId: clientConfig.clientId,
|
|
444
456
|
refreshToken: clientConfig.refreshToken,
|
|
@@ -448,7 +460,7 @@ export class Client {
|
|
|
448
460
|
);
|
|
449
461
|
} else if (clientConfig.externalJwt) {
|
|
450
462
|
//Are we exchanging a JWT previously issued by a trusted external entity (e.g. Google) for a bearer token?
|
|
451
|
-
|
|
463
|
+
authProvider = new OIDCExternalJwtProvider(
|
|
452
464
|
{
|
|
453
465
|
clientId: clientConfig.clientId,
|
|
454
466
|
externalJwt: clientConfig.externalJwt,
|
|
@@ -458,11 +470,25 @@ export class Client {
|
|
|
458
470
|
);
|
|
459
471
|
}
|
|
460
472
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
})
|
|
473
|
+
|
|
474
|
+
// Resolve to AuthConfig: interceptors take precedence over authProvider.
|
|
475
|
+
if (config.interceptors?.length) {
|
|
476
|
+
this.auth = { interceptors: config.interceptors };
|
|
477
|
+
} else if (authProvider) {
|
|
478
|
+
this.auth = authProvider;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (config.interceptors?.length && !authProvider) {
|
|
482
|
+
// Interceptor path: no updateClientPublicKey needed.
|
|
483
|
+
// Still need dpopKeys for request body signing (reqSignature).
|
|
484
|
+
this.dpopKeys = clientConfig.dpopKeys ?? this.cryptoService.generateSigningKeyPair();
|
|
485
|
+
} else {
|
|
486
|
+
this.dpopKeys = createSessionKeys({
|
|
487
|
+
authProvider,
|
|
488
|
+
cryptoService: this.cryptoService,
|
|
489
|
+
dpopKeys: clientConfig.dpopKeys,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
466
492
|
}
|
|
467
493
|
|
|
468
494
|
/** Necessary only for testing. A dependency-injection approach should be preferred, but that is difficult currently */
|
|
@@ -566,9 +592,12 @@ export class Client {
|
|
|
566
592
|
if (!this.platformUrl) {
|
|
567
593
|
throw new ConfigurationError('platformUrl not set in TDF3 Client constructor');
|
|
568
594
|
}
|
|
595
|
+
if (!this.auth) {
|
|
596
|
+
throw new ConfigurationError('AuthProvider or interceptors required for autoconfigure');
|
|
597
|
+
}
|
|
569
598
|
const fetchedFQNValues = await attributeFQNsAsValues(
|
|
570
599
|
this.platformUrl,
|
|
571
|
-
this.
|
|
600
|
+
this.auth,
|
|
572
601
|
...fqnsWithoutValues
|
|
573
602
|
);
|
|
574
603
|
fetchedFQNValues.forEach((fetchedValue) => {
|
|
@@ -739,7 +768,7 @@ export class Client {
|
|
|
739
768
|
contentStream: opts.source,
|
|
740
769
|
mimeType,
|
|
741
770
|
policy: policyObject,
|
|
742
|
-
|
|
771
|
+
auth: this.auth,
|
|
743
772
|
progressHandler: this.clientConfig.progressHandler,
|
|
744
773
|
keyForEncryption,
|
|
745
774
|
keyForManifest,
|
|
@@ -775,14 +804,14 @@ export class Client {
|
|
|
775
804
|
fulfillableObligationFQNs = [],
|
|
776
805
|
}: DecryptParams): Promise<DecoratedReadableStream> {
|
|
777
806
|
const dpopKeys = await this.dpopKeys;
|
|
778
|
-
if (!this.
|
|
779
|
-
throw new ConfigurationError('AuthProvider missing');
|
|
807
|
+
if (!this.auth) {
|
|
808
|
+
throw new ConfigurationError('AuthProvider or interceptors missing');
|
|
780
809
|
}
|
|
781
810
|
const chunker = await makeChunkable(source);
|
|
782
811
|
if (!allowList && this.allowedKases) {
|
|
783
812
|
allowList = this.allowedKases;
|
|
784
813
|
} else if (this.platformUrl) {
|
|
785
|
-
allowList = await fetchKeyAccessServers(this.platformUrl, this.
|
|
814
|
+
allowList = await fetchKeyAccessServers(this.platformUrl, this.auth);
|
|
786
815
|
} else {
|
|
787
816
|
throw new ConfigurationError('platformUrl is required when allowedKases is empty');
|
|
788
817
|
}
|
|
@@ -798,7 +827,7 @@ export class Client {
|
|
|
798
827
|
return await (streamMiddleware as DecryptStreamMiddleware)(
|
|
799
828
|
await readStream({
|
|
800
829
|
allowList,
|
|
801
|
-
|
|
830
|
+
auth: this.auth,
|
|
802
831
|
chunker,
|
|
803
832
|
concurrencyLimit,
|
|
804
833
|
cryptoService: this.cryptoService,
|
package/tdf3/src/tdf.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
UnsignedRewrapRequest_WithKeyAccessObjectSchema,
|
|
16
16
|
} from '../../src/platform/kas/kas_pb.js';
|
|
17
17
|
import { type AuthProvider, reqSignature } from '../../src/auth/auth.js';
|
|
18
|
+
import { type AuthConfig } from '../../src/auth/interceptors.js';
|
|
18
19
|
import { handleRpcRewrapErrorString } from '../../src/access/access-rpc.js';
|
|
19
20
|
import { allPool, anyPool } from '../../src/concurrency.js';
|
|
20
21
|
import { base64, hex } from '../../src/encodings/index.js';
|
|
@@ -152,7 +153,8 @@ export type EncryptConfiguration = {
|
|
|
152
153
|
contentStream: ReadableStream<Uint8Array>;
|
|
153
154
|
mimeType?: string;
|
|
154
155
|
policy: Policy;
|
|
155
|
-
|
|
156
|
+
/** Auth configuration: AuthProvider or { interceptors }. */
|
|
157
|
+
auth?: AuthConfig;
|
|
156
158
|
byteLimit: number;
|
|
157
159
|
progressHandler?: (bytesProcessed: number) => void;
|
|
158
160
|
keyForEncryption: KeyInfo;
|
|
@@ -166,7 +168,8 @@ export type DecryptConfiguration = {
|
|
|
166
168
|
fulfillableObligations: string[];
|
|
167
169
|
allowedKases?: string[];
|
|
168
170
|
allowList?: OriginAllowList;
|
|
169
|
-
|
|
171
|
+
/** Auth configuration: AuthProvider or { interceptors }. */
|
|
172
|
+
auth?: AuthConfig;
|
|
170
173
|
cryptoService: CryptoService;
|
|
171
174
|
|
|
172
175
|
dpopKeys: KeyPair;
|
|
@@ -371,7 +374,7 @@ function isTargetSpecLegacyTDF(targetSpecVersion?: string): boolean {
|
|
|
371
374
|
}
|
|
372
375
|
|
|
373
376
|
export async function writeStream(cfg: EncryptConfiguration): Promise<DecoratedReadableStream> {
|
|
374
|
-
if (!cfg.
|
|
377
|
+
if (!cfg.auth) {
|
|
375
378
|
throw new ConfigurationError('No authorization middleware defined');
|
|
376
379
|
}
|
|
377
380
|
if (!cfg.contentStream) {
|
|
@@ -737,7 +740,7 @@ type RewrapResponseData = {
|
|
|
737
740
|
async function unwrapKey({
|
|
738
741
|
manifest,
|
|
739
742
|
allowedKases,
|
|
740
|
-
|
|
743
|
+
auth,
|
|
741
744
|
dpopKeys,
|
|
742
745
|
concurrencyLimit,
|
|
743
746
|
cryptoService,
|
|
@@ -746,18 +749,18 @@ async function unwrapKey({
|
|
|
746
749
|
}: {
|
|
747
750
|
manifest: Manifest;
|
|
748
751
|
allowedKases: OriginAllowList;
|
|
749
|
-
|
|
752
|
+
/** Auth configuration: AuthProvider or { interceptors }. */
|
|
753
|
+
auth?: AuthConfig;
|
|
750
754
|
concurrencyLimit?: number;
|
|
751
755
|
dpopKeys: KeyPair;
|
|
752
756
|
cryptoService: CryptoService;
|
|
753
757
|
wrappingKeyAlgorithm?: KasPublicKeyAlgorithm;
|
|
754
758
|
fulfillableObligations: string[];
|
|
755
759
|
}) {
|
|
756
|
-
if (
|
|
757
|
-
throw new ConfigurationError(
|
|
758
|
-
'rewrap requires auth provider; must be configured in client constructor'
|
|
759
|
-
);
|
|
760
|
+
if (!auth) {
|
|
761
|
+
throw new ConfigurationError('rewrap requires auth; must be configured in client constructor');
|
|
760
762
|
}
|
|
763
|
+
const resolvedAuth: AuthConfig = auth;
|
|
761
764
|
const { keyAccess } = manifest.encryptionInformation;
|
|
762
765
|
const splitPotentials = splitLookupTableFactory(keyAccess, allowedKases);
|
|
763
766
|
|
|
@@ -829,7 +832,7 @@ async function unwrapKey({
|
|
|
829
832
|
const rewrapResp = await fetchWrappedKey(
|
|
830
833
|
url,
|
|
831
834
|
signedRequestToken,
|
|
832
|
-
|
|
835
|
+
resolvedAuth,
|
|
833
836
|
fulfillableObligations
|
|
834
837
|
);
|
|
835
838
|
// Upgrade V1 response to V2 format if needed
|
|
@@ -1143,7 +1146,7 @@ export async function decryptStreamFrom(
|
|
|
1143
1146
|
const { metadata, reconstructedKey, requiredObligations } = await unwrapKey({
|
|
1144
1147
|
fulfillableObligations: cfg.fulfillableObligations,
|
|
1145
1148
|
manifest,
|
|
1146
|
-
|
|
1149
|
+
auth: cfg.auth,
|
|
1147
1150
|
allowedKases: allowList,
|
|
1148
1151
|
dpopKeys: cfg.dpopKeys,
|
|
1149
1152
|
cryptoService: cfg.cryptoService,
|