@mcp-abap-adt/auth-broker 3.1.0 → 4.1.0
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/CHANGELOG.md +531 -0
- package/README.md +1062 -220
- package/dist/AuthBroker.d.ts +249 -30
- package/dist/AuthBroker.d.ts.map +1 -1
- package/dist/AuthBroker.js +681 -115
- package/dist/DestinationConfigError.d.ts +27 -0
- package/dist/DestinationConfigError.d.ts.map +1 -0
- package/dist/DestinationConfigError.js +36 -0
- package/dist/SessionWriter.d.ts +54 -0
- package/dist/SessionWriter.d.ts.map +1 -0
- package/dist/SessionWriter.js +135 -0
- package/dist/binding.d.ts +117 -0
- package/dist/binding.d.ts.map +1 -0
- package/dist/binding.js +223 -0
- package/dist/bindingOf.d.ts +37 -0
- package/dist/bindingOf.d.ts.map +1 -0
- package/dist/bindingOf.js +53 -0
- package/dist/clientAuthentication.d.ts +109 -0
- package/dist/clientAuthentication.d.ts.map +1 -0
- package/dist/clientAuthentication.js +218 -0
- package/dist/destinations.d.ts +164 -0
- package/dist/destinations.d.ts.map +1 -0
- package/dist/destinations.js +473 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -7
- package/dist/providers/ITokenProvider.d.ts +2 -1
- package/dist/providers/ITokenProvider.d.ts.map +1 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.js +2 -2
- package/dist/stores/index.d.ts +2 -2
- package/dist/stores/index.js +2 -2
- package/dist/stores/interfaces.d.ts +4 -3
- package/dist/stores/interfaces.d.ts.map +1 -1
- package/dist/stores/interfaces.js +2 -2
- package/dist/types.d.ts +5 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -1
- package/package.json +16 -8
package/dist/AuthBroker.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* AuthBroker:
|
|
3
|
+
* AuthBroker: the credential for a destination, and tokens for one.
|
|
4
|
+
*
|
|
5
|
+
* `getProvider` builds the `IAuthProvider` a destination states, from the
|
|
6
|
+
* means its service key store holds and the secret its session store holds.
|
|
7
|
+
* Every token provider it builds writes what it obtains back to the session
|
|
8
|
+
* store through `onTokens` — the secret alone, retried by the broker when the
|
|
9
|
+
* store fails (`SessionWriter`); `flush()` reports what is still pending.
|
|
10
|
+
* The token API (`getToken`, `refreshToken`, `createTokenRefresher`) asks that
|
|
11
|
+
* same provider — one per destination, shared — or, when the consumer gives
|
|
12
|
+
* one, the consumer's provider, whose every answer it writes through the same
|
|
13
|
+
* path.
|
|
4
14
|
*
|
|
5
15
|
* The broker orchestrates and nothing more. It resolves what the stores know
|
|
6
16
|
* about a destination, hands it to the provider, asks the provider for a token
|
|
@@ -12,12 +22,111 @@
|
|
|
12
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
23
|
exports.AuthBroker = void 0;
|
|
14
24
|
const interfaces_auth_1 = require("@mcp-abap-adt/interfaces-auth");
|
|
25
|
+
const binding_1 = require("./binding");
|
|
26
|
+
const bindingOf_1 = require("./bindingOf");
|
|
27
|
+
const clientAuthentication_1 = require("./clientAuthentication");
|
|
28
|
+
const DestinationConfigError_1 = require("./DestinationConfigError");
|
|
29
|
+
const destinations_1 = require("./destinations");
|
|
30
|
+
const SessionWriter_1 = require("./SessionWriter");
|
|
15
31
|
const noOpLogger = {
|
|
16
32
|
info: () => { },
|
|
17
33
|
error: () => { },
|
|
18
34
|
warn: () => { },
|
|
19
35
|
debug: () => { },
|
|
20
36
|
};
|
|
37
|
+
/** The session secret's fields on a connection config — never means. */
|
|
38
|
+
/** The session's side: the secret and what it is bound to. */
|
|
39
|
+
const SECRET_FIELDS = [
|
|
40
|
+
'authorizationToken',
|
|
41
|
+
'sessionCookies',
|
|
42
|
+
'expiresAt',
|
|
43
|
+
'issuedFor',
|
|
44
|
+
'issuedBy',
|
|
45
|
+
];
|
|
46
|
+
const isSecretField = (field) => SECRET_FIELDS.includes(field);
|
|
47
|
+
/**
|
|
48
|
+
* The connection means a strategy-path seed carries — the connection's own,
|
|
49
|
+
* none of them a secret. Everything else a store's connection read answers
|
|
50
|
+
* (credentials, grant data, fields outside the type) stays behind.
|
|
51
|
+
*/
|
|
52
|
+
const SEED_MEANS = [
|
|
53
|
+
'serviceUrl',
|
|
54
|
+
'sapClient',
|
|
55
|
+
'language',
|
|
56
|
+
'authType',
|
|
57
|
+
'grantType',
|
|
58
|
+
];
|
|
59
|
+
/**
|
|
60
|
+
* The token API factory's seed on the `clientAuthentication` strategy path,
|
|
61
|
+
* from an allowlist only: `SEED_MEANS`, and the session secret's own fields
|
|
62
|
+
* (`SECRET_FIELDS`: the token or cookies, the expiry, the binding) only when
|
|
63
|
+
* that same read is bound here — each secret judged by the read it came from.
|
|
64
|
+
* `serviceUrl` is the one resolved for the destination.
|
|
65
|
+
*/
|
|
66
|
+
function strategySeed(connConfig, binding, serviceUrl) {
|
|
67
|
+
const source = (connConfig ?? {});
|
|
68
|
+
const fields = connConfig && (0, binding_1.boundHere)(connConfig, binding)
|
|
69
|
+
? [...SEED_MEANS, ...SECRET_FIELDS]
|
|
70
|
+
: SEED_MEANS;
|
|
71
|
+
const seed = {};
|
|
72
|
+
for (const field of fields) {
|
|
73
|
+
if (source[field] !== undefined)
|
|
74
|
+
seed[field] = source[field];
|
|
75
|
+
}
|
|
76
|
+
return { ...seed, serviceUrl };
|
|
77
|
+
}
|
|
78
|
+
/** When the result expires, in epoch ms: its own `expiresAt`, else `expiresIn` from now. */
|
|
79
|
+
function expiryOf(result) {
|
|
80
|
+
if (typeof result.expiresAt === 'number')
|
|
81
|
+
return result.expiresAt;
|
|
82
|
+
if (typeof result.expiresIn === 'number') {
|
|
83
|
+
return Date.now() + Math.round(result.expiresIn * 1000);
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
/** A provider `getProvider` built that obtains tokens: a token provider. */
|
|
88
|
+
function obtainsTokens(provider) {
|
|
89
|
+
const candidate = provider;
|
|
90
|
+
return (typeof candidate.getTokens === 'function' &&
|
|
91
|
+
typeof candidate.refreshTokens === 'function');
|
|
92
|
+
}
|
|
93
|
+
/** The refresh token a stored session holds, unchecked. */
|
|
94
|
+
function storedRefreshTokenOf(session) {
|
|
95
|
+
return typeof session?.refreshToken === 'string'
|
|
96
|
+
? session.refreshToken
|
|
97
|
+
: undefined;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The 3.x composition, the no-strategy path: the session's client as it is,
|
|
101
|
+
* else the key store's with `refreshToken` — the one given, else the key's
|
|
102
|
+
* own. (The strategy path takes `strategyAuthorization`.)
|
|
103
|
+
*/
|
|
104
|
+
function composeAuthorization(read, refreshToken) {
|
|
105
|
+
if (read.sessionAuth)
|
|
106
|
+
return read.sessionAuth;
|
|
107
|
+
if (!read.keyAuth)
|
|
108
|
+
return null;
|
|
109
|
+
return {
|
|
110
|
+
...read.keyAuth,
|
|
111
|
+
refreshToken: refreshToken ?? read.keyAuth.refreshToken,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The client on the `clientAuthentication` strategy path: the session's own
|
|
116
|
+
* client, else the key store's, carrying only `refreshToken` — the one the
|
|
117
|
+
* session read whose binding was checked holds, when bound — never a refresh
|
|
118
|
+
* token either client read carried itself.
|
|
119
|
+
*/
|
|
120
|
+
function strategyAuthorization(read, refreshToken) {
|
|
121
|
+
// The same allowlist the strategy is told (`contextClient`): nothing else a
|
|
122
|
+
// store's authorization-config read carried reaches the factory.
|
|
123
|
+
const client = (0, clientAuthentication_1.contextClient)(read.sessionAuth ?? read.keyAuth);
|
|
124
|
+
return client ? { ...client, refreshToken } : null;
|
|
125
|
+
}
|
|
126
|
+
/** A stored string that counts as present: `''` is none. */
|
|
127
|
+
function present(value) {
|
|
128
|
+
return typeof value === 'string' && value !== '';
|
|
129
|
+
}
|
|
21
130
|
function errorCode(error) {
|
|
22
131
|
if (error !== null && typeof error === 'object' && 'code' in error) {
|
|
23
132
|
const code = error.code;
|
|
@@ -25,6 +134,13 @@ function errorCode(error) {
|
|
|
25
134
|
}
|
|
26
135
|
return undefined;
|
|
27
136
|
}
|
|
137
|
+
/** A result carrying a token, or the 3.x error for one that carries none. */
|
|
138
|
+
function checked(destination, result) {
|
|
139
|
+
if (!result?.authorizationToken) {
|
|
140
|
+
throw new Error(`Token provider did not return authorization token for destination "${destination}"`);
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
28
144
|
/**
|
|
29
145
|
* AuthBroker manages authentication tokens for destinations
|
|
30
146
|
*/
|
|
@@ -33,7 +149,30 @@ class AuthBroker {
|
|
|
33
149
|
serviceKeyStore;
|
|
34
150
|
sessionStore;
|
|
35
151
|
provider;
|
|
36
|
-
providers
|
|
152
|
+
/** A consumer factory's providers: the promise of the build, as `built`. */
|
|
153
|
+
consumerBuilt = new Map();
|
|
154
|
+
/**
|
|
155
|
+
* One cache per destination, shared by `getProvider` and — when the
|
|
156
|
+
* consumer gives no provider — the token API: the promise of the
|
|
157
|
+
* build, set before its first read.
|
|
158
|
+
*/
|
|
159
|
+
built = new Map();
|
|
160
|
+
/**
|
|
161
|
+
* The write that failed for a result, keyed on the result the provider
|
|
162
|
+
* handed over and returns, then on the destination it was written
|
|
163
|
+
* for: the token API throws it to its caller, as 3.x did, while the broker
|
|
164
|
+
* keeps retrying. Dropped once a write of that result for that destination
|
|
165
|
+
* lands.
|
|
166
|
+
*/
|
|
167
|
+
failedWrites = new WeakMap();
|
|
168
|
+
authorization;
|
|
169
|
+
oidcAuthorization;
|
|
170
|
+
deviceCodePresenter;
|
|
171
|
+
samlCookies;
|
|
172
|
+
assertionReplayStore;
|
|
173
|
+
clientAuthentication;
|
|
174
|
+
/** getProvider's writes of the session secret, retried on their own. */
|
|
175
|
+
writer;
|
|
37
176
|
/**
|
|
38
177
|
* @param config Stores and the provider (instance or factory)
|
|
39
178
|
* @param logger Optional logger. Nothing the broker logs contains a token.
|
|
@@ -46,9 +185,6 @@ class AuthBroker {
|
|
|
46
185
|
if (!sessionStore) {
|
|
47
186
|
throw new Error('AuthBroker: sessionStore is required');
|
|
48
187
|
}
|
|
49
|
-
if (!provider) {
|
|
50
|
-
throw new Error('AuthBroker: provider is required');
|
|
51
|
-
}
|
|
52
188
|
for (const method of [
|
|
53
189
|
'getAuthorizationConfig',
|
|
54
190
|
'getConnectionConfig',
|
|
@@ -61,7 +197,7 @@ class AuthBroker {
|
|
|
61
197
|
throw new Error(`AuthBroker: sessionStore.${method} must be a function`);
|
|
62
198
|
}
|
|
63
199
|
}
|
|
64
|
-
if (typeof provider !== 'function') {
|
|
200
|
+
if (provider && typeof provider !== 'function') {
|
|
65
201
|
if (typeof provider.getTokens !== 'function') {
|
|
66
202
|
throw new Error('AuthBroker: provider.getTokens must be a function');
|
|
67
203
|
}
|
|
@@ -83,91 +219,303 @@ class AuthBroker {
|
|
|
83
219
|
this.sessionStore = sessionStore;
|
|
84
220
|
this.serviceKeyStore = serviceKeyStore;
|
|
85
221
|
this.provider = provider;
|
|
222
|
+
this.authorization = config.authorization;
|
|
223
|
+
this.oidcAuthorization = config.oidcAuthorization;
|
|
224
|
+
this.deviceCodePresenter = config.deviceCodePresenter;
|
|
225
|
+
this.samlCookies = config.samlCookies;
|
|
226
|
+
this.assertionReplayStore = config.assertionReplayStore;
|
|
227
|
+
this.clientAuthentication = config.clientAuthentication;
|
|
86
228
|
this.logger = logger ?? noOpLogger;
|
|
229
|
+
this.writer = new SessionWriter_1.SessionWriter(async (destination, bound) => {
|
|
230
|
+
try {
|
|
231
|
+
await this.writeSecret(destination, bound);
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
// Per destination: one result object may be answered for several
|
|
235
|
+
// (an instance serves every destination), and a write that lands for
|
|
236
|
+
// one says nothing about another's.
|
|
237
|
+
let failures = this.failedWrites.get(bound.original);
|
|
238
|
+
if (!failures) {
|
|
239
|
+
failures = new Map();
|
|
240
|
+
this.failedWrites.set(bound.original, failures);
|
|
241
|
+
}
|
|
242
|
+
failures.set(destination, { error });
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
this.failedWrites.get(bound.original)?.delete(destination);
|
|
246
|
+
}, this.logger);
|
|
87
247
|
this.logger.debug('[AuthBroker] Broker initialized', {
|
|
88
248
|
hasServiceKeyStore: !!serviceKeyStore,
|
|
89
|
-
providerForm:
|
|
249
|
+
providerForm: provider === undefined
|
|
250
|
+
? 'none'
|
|
251
|
+
: typeof provider === 'function'
|
|
252
|
+
? 'factory'
|
|
253
|
+
: 'instance',
|
|
90
254
|
});
|
|
91
255
|
}
|
|
92
256
|
/**
|
|
93
257
|
* A token for the destination: the provider's current one, which it refreshes
|
|
94
258
|
* or obtains by login when it judges the cached one unusable.
|
|
95
259
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
260
|
+
* Without a consumer `provider`, the provider is the one `getProvider` hands
|
|
261
|
+
* out for the destination — the same instance, so one token and one renewal
|
|
262
|
+
* serve both — and what it obtains is written by its `onTokens`. With one,
|
|
263
|
+
* the consumer's provider is asked and its answer written, as in 3.x — the
|
|
264
|
+
* secret alone.
|
|
265
|
+
*
|
|
266
|
+
* Errors from the provider (its typed errors included) propagate unchanged;
|
|
267
|
+
* so does a store failure, a failed write of the token included — the token
|
|
268
|
+
* stands and the broker keeps retrying the write.
|
|
269
|
+
*
|
|
270
|
+
* @throws DestinationConfigError for a destination the key store states as
|
|
271
|
+
* `basic` or `snc`, before any provider is asked; without a consumer
|
|
272
|
+
* `provider`, also for one whose provider obtains no token.
|
|
98
273
|
*/
|
|
99
274
|
async getToken(destination) {
|
|
100
275
|
return this.obtain(destination, 'getTokens');
|
|
101
276
|
}
|
|
102
277
|
/**
|
|
103
278
|
* A new token for the destination, never the cached one — for a caller whose
|
|
104
|
-
* token the server has just refused. Calls the provider's `refreshTokens()
|
|
105
|
-
*
|
|
279
|
+
* token the server has just refused. Calls the provider's `refreshTokens()`;
|
|
280
|
+
* a renewal already in flight for the destination is joined, not repeated.
|
|
106
281
|
*/
|
|
107
282
|
async refreshToken(destination) {
|
|
108
283
|
return this.obtain(destination, 'refreshTokens');
|
|
109
284
|
}
|
|
110
285
|
async obtain(destination, method) {
|
|
286
|
+
const means = await this.statedForTokens(destination);
|
|
287
|
+
const result = this.provider
|
|
288
|
+
? await this.obtainFromConsumer(destination, method, means)
|
|
289
|
+
: await this.obtainShared(destination, method, means);
|
|
290
|
+
return result.authorizationToken;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The destination's means, read first: a destination the key store states
|
|
294
|
+
* as `basic` or `snc` holds no token, so the token API refuses it before any
|
|
295
|
+
* provider is asked — it would write a token over a credential that is not
|
|
296
|
+
* one. A key store that states no `authType`, or no key store, is
|
|
297
|
+
* a 3.x setup, served as before.
|
|
298
|
+
*/
|
|
299
|
+
async statedForTokens(destination) {
|
|
300
|
+
const serviceKeyStore = this.serviceKeyStore;
|
|
301
|
+
const means = serviceKeyStore
|
|
302
|
+
? await this.read(destination, 'service key connection config', () => serviceKeyStore.getConnectionConfig(destination))
|
|
303
|
+
: null;
|
|
304
|
+
const authType = means?.authType;
|
|
305
|
+
if (authType === 'basic' || authType === 'snc') {
|
|
306
|
+
throw new DestinationConfigError_1.DestinationConfigError(destination, ['authType'], `the token API serves no ${authType} destination: it holds no token`);
|
|
307
|
+
}
|
|
308
|
+
return means;
|
|
309
|
+
}
|
|
310
|
+
/** The token API on `getProvider`'s provider. */
|
|
311
|
+
async obtainShared(destination, method, means) {
|
|
312
|
+
if (!this.serviceKeyStore) {
|
|
313
|
+
throw new DestinationConfigError_1.DestinationConfigError(destination, ['provider', 'serviceKeyStore'], 'the token API needs the provider option, or a service key store stating a token grant');
|
|
314
|
+
}
|
|
315
|
+
if ((means?.authType === 'jwt' || means?.authType === 'saml') &&
|
|
316
|
+
means.grantType === 'none') {
|
|
317
|
+
throw new DestinationConfigError_1.DestinationConfigError(destination, ['provider'], 'a none destination presents a handed-over credential and obtains no token: use getProvider, or give the token API the provider option');
|
|
318
|
+
}
|
|
319
|
+
const provider = await this.getProvider(destination);
|
|
320
|
+
if (!obtainsTokens(provider)) {
|
|
321
|
+
throw new DestinationConfigError_1.DestinationConfigError(destination, ['provider'], 'the destination states a credential that obtains no token: use getProvider, or give the token API the provider option');
|
|
322
|
+
}
|
|
323
|
+
this.logger.debug(`[AuthBroker] ${method} for ${destination}`);
|
|
324
|
+
const result = checked(destination, await provider[method]());
|
|
325
|
+
// onTokens wrote it, or failed to: the failure is this caller's too.
|
|
326
|
+
this.throwFailedWrite(destination, result);
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* The token API on the consumer's provider, as 3.x: the session read first,
|
|
331
|
+
* then the key store, for `serviceUrl` and the client; every answer — a
|
|
332
|
+
* cache hit included — written, the secret alone, through the broker's one
|
|
333
|
+
* write path.
|
|
334
|
+
*/
|
|
335
|
+
async obtainFromConsumer(destination, method, means) {
|
|
111
336
|
const connConfig = await this.read(destination, 'session connection config', () => this.sessionStore.getConnectionConfig(destination));
|
|
112
337
|
const serviceUrl = await this.resolveServiceUrl(destination, connConfig);
|
|
113
|
-
const provider = await this.
|
|
338
|
+
const { provider, binding, carry } = await this.consumerProviderFor(destination, serviceUrl, connConfig, means);
|
|
114
339
|
this.logger.debug(`[AuthBroker] ${method} for ${destination}`);
|
|
115
|
-
const result = await provider[method]();
|
|
116
|
-
|
|
117
|
-
|
|
340
|
+
const result = checked(destination, await provider[method]());
|
|
341
|
+
await this.writer.submit(destination, {
|
|
342
|
+
result: { ...result, expiresAt: expiryOf(result) },
|
|
343
|
+
original: result,
|
|
344
|
+
binding,
|
|
345
|
+
carry,
|
|
346
|
+
});
|
|
347
|
+
this.throwFailedWrite(destination, result);
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
/** The failure recorded for this result's write here, thrown as the store raised it. */
|
|
351
|
+
throwFailedWrite(destination, result) {
|
|
352
|
+
const failed = this.failedWrites.get(result)?.get(destination);
|
|
353
|
+
if (failed) {
|
|
354
|
+
throw failed.error;
|
|
118
355
|
}
|
|
119
|
-
await this.persist(destination, serviceUrl, connConfig, result);
|
|
120
|
-
return result.authorizationToken;
|
|
121
356
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
357
|
+
/**
|
|
358
|
+
* The consumer's provider for the destination, cached per destination as a
|
|
359
|
+
* promise set before its first read and dropped when it throws, so
|
|
360
|
+
* the next call builds again: a factory's build — handed the client resolved
|
|
361
|
+
* then — or an instance as given, for every destination, handed no client.
|
|
362
|
+
* Either way the binding is fixed here, from the `serviceUrl`, SAP client and
|
|
363
|
+
* client of this first call, and kept with the provider.
|
|
364
|
+
*/
|
|
365
|
+
consumerProviderFor(destination, serviceUrl, connConfig, means) {
|
|
366
|
+
const cached = this.consumerBuilt.get(destination);
|
|
367
|
+
if (cached) {
|
|
368
|
+
return cached;
|
|
369
|
+
}
|
|
370
|
+
const provider = this.provider;
|
|
371
|
+
const sapClient = present(connConfig?.sapClient)
|
|
372
|
+
? connConfig.sapClient
|
|
373
|
+
: means?.sapClient;
|
|
374
|
+
const build = Promise.resolve().then(async () => {
|
|
375
|
+
if (typeof provider !== 'function') {
|
|
376
|
+
return {
|
|
377
|
+
provider,
|
|
378
|
+
binding: (0, binding_1.consumerBinding)(serviceUrl, sapClient, null),
|
|
379
|
+
carry: 'any',
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const read = await this.readAuthorization(destination);
|
|
383
|
+
const strategic = await this.consumerClientAuthentication(destination, means,
|
|
384
|
+
// The strategy is told the client, never a refresh token read with it.
|
|
385
|
+
strategyAuthorization(read, undefined));
|
|
386
|
+
let built;
|
|
387
|
+
let client;
|
|
388
|
+
let binding;
|
|
389
|
+
let carry;
|
|
390
|
+
if (strategic) {
|
|
391
|
+
// The strategy's answer and the client identity, resolved before the
|
|
392
|
+
// factory; the factory called inside the same guard: a throw is fixed
|
|
393
|
+
// words, nothing of the thrown value, no cause. The stored refresh
|
|
394
|
+
// token is carried only when the session is bound to this resource
|
|
395
|
+
// and this client identity — never to another authorization server.
|
|
396
|
+
binding = (0, binding_1.strategyBinding)((0, binding_1.consumerBinding)(serviceUrl, sapClient, strategic.identity));
|
|
397
|
+
const session = read.sessionRead
|
|
398
|
+
? read.session
|
|
399
|
+
: await this.loadStoredSession(destination);
|
|
400
|
+
const bound = !!session && (0, binding_1.boundHere)(session, binding);
|
|
401
|
+
const boundRefreshToken = bound
|
|
402
|
+
? storedRefreshTokenOf(session)
|
|
403
|
+
: undefined;
|
|
404
|
+
// The client without any refresh token of its own read: a session
|
|
405
|
+
// store's `getAuthorizationConfig` is another read than the session
|
|
406
|
+
// whose binding was checked, and another process may have written
|
|
407
|
+
// between them. The refresh token comes only from that checked read.
|
|
408
|
+
client = strategyAuthorization(read, boundRefreshToken);
|
|
409
|
+
// Each stored secret is judged by the read it came from: the seed's
|
|
410
|
+
// token, cookies and expiry by the binding the connection read itself
|
|
411
|
+
// carries — another process may have written between the two reads —
|
|
412
|
+
// else only the means seed the provider.
|
|
413
|
+
const seed = strategySeed(connConfig, binding, serviceUrl);
|
|
414
|
+
carry = 'bound';
|
|
415
|
+
const refreshToken = client ? client.refreshToken : boundRefreshToken;
|
|
416
|
+
const fourth = {
|
|
417
|
+
clientAuthentication: strategic.clientAuthentication,
|
|
418
|
+
...(strategic.identity
|
|
419
|
+
? {
|
|
420
|
+
uaaUrl: strategic.identity.uaaUrl,
|
|
421
|
+
clientId: strategic.identity.uaaClientId,
|
|
422
|
+
}
|
|
423
|
+
: {}),
|
|
424
|
+
...(present(refreshToken) ? { refreshToken } : {}),
|
|
425
|
+
};
|
|
426
|
+
try {
|
|
427
|
+
built = provider(destination, client, seed, fourth);
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
throw new DestinationConfigError_1.DestinationConfigError(destination, ['provider', 'clientAuthentication'], 'the token provider factory failed beside the clientAuthentication strategy');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
// 4.0.0: the stored secret and refresh token carried over as read.
|
|
435
|
+
client = composeAuthorization(read, storedRefreshTokenOf(read.session));
|
|
436
|
+
binding = (0, binding_1.consumerBinding)(serviceUrl, sapClient, client);
|
|
437
|
+
carry = 'any';
|
|
438
|
+
built = provider(destination, client, {
|
|
439
|
+
...(connConfig ?? {}),
|
|
440
|
+
serviceUrl,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
this.logger.debug(`[AuthBroker] Provider built for ${destination}`, {
|
|
444
|
+
hasCredentials: !!client,
|
|
445
|
+
hasRefreshToken: !!client?.refreshToken,
|
|
446
|
+
hasStoredToken: !!(connConfig?.authorizationToken || connConfig?.sessionCookies),
|
|
447
|
+
});
|
|
448
|
+
return { provider: built, binding, carry };
|
|
134
449
|
});
|
|
135
|
-
this.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
450
|
+
this.consumerBuilt.set(destination, build);
|
|
451
|
+
build.catch(() => {
|
|
452
|
+
if (this.consumerBuilt.get(destination) === build) {
|
|
453
|
+
this.consumerBuilt.delete(destination);
|
|
454
|
+
}
|
|
140
455
|
});
|
|
141
|
-
return
|
|
456
|
+
return build;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* The token API's strategy path: with a `clientAuthentication` strategy and a
|
|
460
|
+
* destination stating a grant whose client authenticates, the strategy's
|
|
461
|
+
* answer — resolved inside the guard — and the client identity: the secret
|
|
462
|
+
* client, else the certificate client's, read through the context's memoised
|
|
463
|
+
* `readCertificate`. `null` without a strategy (4.0.0: nothing
|
|
464
|
+
* certificate-related is asked) and for a grant that authenticates no
|
|
465
|
+
* client (`saml2_pure`, `none`), as `getProvider` decides.
|
|
466
|
+
*
|
|
467
|
+
* The strategy is told the grant, so a destination that states none is
|
|
468
|
+
* refused before it is called rather than built without it. (An instance
|
|
469
|
+
* given as `provider` is the consumer's own composition: the strategy
|
|
470
|
+
* applies only to what the broker builds, so it is not used there.)
|
|
471
|
+
*/
|
|
472
|
+
async consumerClientAuthentication(destination, means, client) {
|
|
473
|
+
const strategy = this.clientAuthentication;
|
|
474
|
+
if (!strategy) {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
// basic and snc never get here: the token API refuses them first
|
|
478
|
+
// (`statedForTokens`).
|
|
479
|
+
if (!means || (means.authType !== 'jwt' && means.authType !== 'saml')) {
|
|
480
|
+
throw new DestinationConfigError_1.DestinationConfigError(destination, ['authType', 'grantType'], 'a clientAuthentication strategy is told the grant: the destination must state its authType and grantType');
|
|
481
|
+
}
|
|
482
|
+
const grant = (0, destinations_1.statedGrant)(destination, means.authType, means);
|
|
483
|
+
if (grant === 'saml2_pure' || grant === 'none') {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
const serviceKeyStore = this.serviceKeyStore;
|
|
487
|
+
const context = (0, clientAuthentication_1.clientAuthenticationContext)(destination, grant, client, () => this.read(destination, 'client certificate', async () => serviceKeyStore?.getClientCertificate
|
|
488
|
+
? serviceKeyStore.getClientCertificate(destination)
|
|
489
|
+
: null));
|
|
490
|
+
const clientAuthentication = await (0, clientAuthentication_1.resolveClientAuthentication)(strategy, context);
|
|
491
|
+
return { clientAuthentication, identity: await (0, clientAuthentication_1.clientIdentity)(context) };
|
|
142
492
|
}
|
|
143
493
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* client secret into it; `loadSession` is where such a token is read.
|
|
494
|
+
* What a consumer's factory's credentials are composed from, read in the
|
|
495
|
+
* 3.x order: the session's authorization config; only when it has none, the
|
|
496
|
+
* session and then the key store's client. A session store of auth-stores 3
|
|
497
|
+
* answers no client, so the key store's is what is found.
|
|
149
498
|
*/
|
|
150
|
-
async
|
|
499
|
+
async readAuthorization(destination) {
|
|
151
500
|
const sessionAuth = await this.read(destination, 'session authorization config', () => this.sessionStore.getAuthorizationConfig(destination));
|
|
152
501
|
if (sessionAuth) {
|
|
153
|
-
return sessionAuth;
|
|
502
|
+
return { sessionAuth, keyAuth: null, session: null, sessionRead: false };
|
|
154
503
|
}
|
|
155
|
-
const session = await this.
|
|
156
|
-
const storedRefreshToken = typeof session?.refreshToken === 'string'
|
|
157
|
-
? session.refreshToken
|
|
158
|
-
: undefined;
|
|
504
|
+
const session = await this.loadStoredSession(destination);
|
|
159
505
|
const serviceKeyStore = this.serviceKeyStore;
|
|
160
506
|
const keyAuth = serviceKeyStore
|
|
161
507
|
? await this.read(destination, 'service key authorization config', () => serviceKeyStore.getAuthorizationConfig(destination))
|
|
162
508
|
: null;
|
|
163
|
-
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
return {
|
|
167
|
-
...keyAuth,
|
|
168
|
-
refreshToken: storedRefreshToken ?? keyAuth.refreshToken,
|
|
169
|
-
};
|
|
509
|
+
return { sessionAuth: null, keyAuth, session, sessionRead: true };
|
|
170
510
|
}
|
|
511
|
+
/** The session the store holds for the destination, or null. */
|
|
512
|
+
loadStoredSession(destination) {
|
|
513
|
+
return this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* `serviceUrl` for a consumer's provider, in the 3.x order: the session's,
|
|
517
|
+
* else the key store's, else an error before the provider is asked.
|
|
518
|
+
*/
|
|
171
519
|
async resolveServiceUrl(destination, connConfig) {
|
|
172
520
|
let serviceUrl = connConfig?.serviceUrl;
|
|
173
521
|
const serviceKeyStore = this.serviceKeyStore;
|
|
@@ -181,53 +529,6 @@ class AuthBroker {
|
|
|
181
529
|
}
|
|
182
530
|
return serviceUrl;
|
|
183
531
|
}
|
|
184
|
-
/**
|
|
185
|
-
* Writes the result by its type: a SAML result is session cookies, anything
|
|
186
|
-
* else a bearer token. The refresh token is written only when the result has
|
|
187
|
-
* one, so a provider that returns none does not erase the stored one.
|
|
188
|
-
*
|
|
189
|
-
* `ITokenResult.expiresAt` has no field in `IConnectionConfig` to go to; the
|
|
190
|
-
* provider seeded with the stored token reads the expiry from the JWT itself.
|
|
191
|
-
*/
|
|
192
|
-
async persist(destination, serviceUrl, connConfig, result) {
|
|
193
|
-
const isSaml = result.tokenType === 'saml';
|
|
194
|
-
await this.sessionStore.setConnectionConfig(destination, {
|
|
195
|
-
...(connConfig ?? {}),
|
|
196
|
-
serviceUrl,
|
|
197
|
-
authorizationToken: isSaml ? undefined : result.authorizationToken,
|
|
198
|
-
sessionCookies: isSaml ? result.authorizationToken : undefined,
|
|
199
|
-
authType: isSaml ? 'saml' : 'jwt',
|
|
200
|
-
});
|
|
201
|
-
if (result.refreshToken) {
|
|
202
|
-
const sessionAuth = await this.read(destination, 'session authorization config', () => this.sessionStore.getAuthorizationConfig(destination));
|
|
203
|
-
if (sessionAuth) {
|
|
204
|
-
// The session holds its own credentials: only the refresh token changes.
|
|
205
|
-
await this.sessionStore.setAuthorizationConfig(destination, {
|
|
206
|
-
...sessionAuth,
|
|
207
|
-
refreshToken: result.refreshToken,
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
// The credentials live in the service key and stay there. The session
|
|
212
|
-
// gets the refresh token alone — never the client secret.
|
|
213
|
-
const session = await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
|
|
214
|
-
await this.sessionStore.saveSession(destination, {
|
|
215
|
-
...(session ?? {}),
|
|
216
|
-
serviceUrl: session?.serviceUrl ?? serviceUrl,
|
|
217
|
-
refreshToken: result.refreshToken,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
this.logger.info(`[AuthBroker] Token saved for ${destination}`, {
|
|
222
|
-
tokenType: result.tokenType ?? 'jwt',
|
|
223
|
-
authType: result.authType,
|
|
224
|
-
hasRefreshToken: !!result.refreshToken,
|
|
225
|
-
expiresAt: result.expiresAt
|
|
226
|
-
? new Date(result.expiresAt).toISOString()
|
|
227
|
-
: undefined,
|
|
228
|
-
expiresIn: result.expiresIn,
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
532
|
/**
|
|
232
533
|
* A store read where absence is an answer and anything else is not.
|
|
233
534
|
*
|
|
@@ -252,34 +553,299 @@ class AuthBroker {
|
|
|
252
553
|
}
|
|
253
554
|
}
|
|
254
555
|
/**
|
|
255
|
-
*
|
|
256
|
-
*
|
|
556
|
+
* The destination's client: the service key store's `uaaUrl`, `uaaClientId`
|
|
557
|
+
* and `uaaClientSecret`, with the refresh token the session stores laid over
|
|
558
|
+
* them; `null` when the key store has no client. A client a session store
|
|
559
|
+
* answers is not read, nor a refresh token a key store answers.
|
|
257
560
|
*/
|
|
258
561
|
async getAuthorizationConfig(destination) {
|
|
259
|
-
const sessionAuth = await this.read(destination, 'session authorization config', () => this.sessionStore.getAuthorizationConfig(destination));
|
|
260
|
-
if (sessionAuth) {
|
|
261
|
-
return sessionAuth;
|
|
262
|
-
}
|
|
263
562
|
const serviceKeyStore = this.serviceKeyStore;
|
|
264
|
-
|
|
563
|
+
const client = serviceKeyStore
|
|
564
|
+
? await this.read(destination, 'service key authorization config', () => serviceKeyStore.getAuthorizationConfig(destination))
|
|
565
|
+
: null;
|
|
566
|
+
if (!client) {
|
|
265
567
|
return null;
|
|
266
568
|
}
|
|
267
|
-
|
|
569
|
+
const secret = await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
|
|
570
|
+
const composed = {
|
|
571
|
+
uaaUrl: client.uaaUrl,
|
|
572
|
+
uaaClientId: client.uaaClientId,
|
|
573
|
+
uaaClientSecret: client.uaaClientSecret,
|
|
574
|
+
};
|
|
575
|
+
if (secret?.refreshToken !== undefined) {
|
|
576
|
+
composed.refreshToken = secret.refreshToken;
|
|
577
|
+
}
|
|
578
|
+
return composed;
|
|
268
579
|
}
|
|
269
580
|
/**
|
|
270
|
-
*
|
|
271
|
-
*
|
|
581
|
+
* The destination's connection config: the service key store's means with
|
|
582
|
+
* the session's secret (`authorizationToken`, `sessionCookies`,
|
|
583
|
+
* `expiresAt`) and its binding (`issuedFor`, `issuedBy`) laid over them; `null` when neither store holds anything.
|
|
584
|
+
* Means a session store answers are not read, nor a secret a key store
|
|
585
|
+
* answers.
|
|
272
586
|
*/
|
|
273
587
|
async getConnectionConfig(destination) {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
588
|
+
const serviceKeyStore = this.serviceKeyStore;
|
|
589
|
+
const means = serviceKeyStore
|
|
590
|
+
? await this.read(destination, 'service key connection config', () => serviceKeyStore.getConnectionConfig(destination))
|
|
591
|
+
: null;
|
|
592
|
+
const secret = await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
|
|
593
|
+
const composed = {};
|
|
594
|
+
for (const [field, value] of Object.entries(means ?? {})) {
|
|
595
|
+
if (!isSecretField(field) && value !== undefined) {
|
|
596
|
+
composed[field] = value;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
for (const field of SECRET_FIELDS) {
|
|
600
|
+
const value = secret?.[field];
|
|
601
|
+
if (value !== undefined) {
|
|
602
|
+
composed[field] = value;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return Object.keys(composed).length > 0 ? composed : null;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* The credential for the destination, ready for a connector: the provider
|
|
609
|
+
* its configuration states (`authType`, and `grantType` for `jwt` / `saml`),
|
|
610
|
+
* built from the means the service key store holds and the secret the
|
|
611
|
+
* session store holds.
|
|
612
|
+
*
|
|
613
|
+
* One provider per destination for the broker's life: concurrent first calls
|
|
614
|
+
* share one build, and a build that threw is tried again on the next call.
|
|
615
|
+
*
|
|
616
|
+
* A token provider (the UAA, OIDC and SAML grants) is seeded from the
|
|
617
|
+
* session when the stored secret is bound to this destination, and writes
|
|
618
|
+
* every token it obtains — at `prepare()`, on expiry, or in `rejected()` after
|
|
619
|
+
* a 401 — back to the session store before it answers (see `flush()`).
|
|
620
|
+
*
|
|
621
|
+
* @throws DestinationConfigError when the destination lacks what its type
|
|
622
|
+
* needs — naming the fields or options, never a value.
|
|
623
|
+
*/
|
|
624
|
+
getProvider(destination) {
|
|
625
|
+
const cached = this.built.get(destination);
|
|
626
|
+
if (cached) {
|
|
627
|
+
return cached;
|
|
277
628
|
}
|
|
629
|
+
// Deferred by one tick, so the promise is in the cache before the build
|
|
630
|
+
// reads anything.
|
|
631
|
+
const build = Promise.resolve().then(() => this.build(destination));
|
|
632
|
+
this.built.set(destination, build);
|
|
633
|
+
build.catch(() => {
|
|
634
|
+
if (this.built.get(destination) === build) {
|
|
635
|
+
this.built.delete(destination);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
return build;
|
|
639
|
+
}
|
|
640
|
+
async build(destination) {
|
|
278
641
|
const serviceKeyStore = this.serviceKeyStore;
|
|
279
642
|
if (!serviceKeyStore) {
|
|
280
|
-
|
|
643
|
+
throw new DestinationConfigError_1.DestinationConfigError(destination, ['serviceKeyStore'], 'getProvider reads the means from a service key store, and none was given');
|
|
644
|
+
}
|
|
645
|
+
const means = await this.read(destination, 'means', () => serviceKeyStore.getConnectionConfig(destination));
|
|
646
|
+
const authType = (0, destinations_1.statedAuthType)(destination, means);
|
|
647
|
+
// statedAuthType refuses a destination without means.
|
|
648
|
+
const stated = means;
|
|
649
|
+
let provider;
|
|
650
|
+
if (authType === 'basic') {
|
|
651
|
+
provider = (0, destinations_1.basicProvider)(destination, stated);
|
|
652
|
+
}
|
|
653
|
+
else if (authType === 'snc') {
|
|
654
|
+
provider = (0, destinations_1.sncProvider)(destination, stated, this.logger);
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
const grant = (0, destinations_1.statedGrant)(destination, authType, stated);
|
|
658
|
+
if (grant !== 'none') {
|
|
659
|
+
// A grant that obtains a secret: the row's client (none for
|
|
660
|
+
// saml2_pure), what a secret for this destination is bound to
|
|
661
|
+
// — written with every secret, and required of a stored one
|
|
662
|
+
// before it seeds — and the stored secret when it is.
|
|
663
|
+
const secretClient = grant === 'saml2_pure'
|
|
664
|
+
? null
|
|
665
|
+
: await this.read(destination, 'client', () => serviceKeyStore.getAuthorizationConfig(destination));
|
|
666
|
+
// The consumer's client authentication, resolved inside the guard
|
|
667
|
+
// before any provider exists: a strategy that throws leaves nothing
|
|
668
|
+
// built and nothing cached. Then the client identity the row and the
|
|
669
|
+
// binding take — the secret client, else the certificate client's —
|
|
670
|
+
// so the binding and the seed are chosen from it. Without a strategy
|
|
671
|
+
// nothing of this runs: 4.0.0's client, nothing certificate-related.
|
|
672
|
+
let client = secretClient;
|
|
673
|
+
let clientAuthentication;
|
|
674
|
+
if (this.clientAuthentication && grant !== 'saml2_pure') {
|
|
675
|
+
const context = (0, clientAuthentication_1.clientAuthenticationContext)(destination, grant, secretClient, () => this.read(destination, 'client certificate', async () => serviceKeyStore.getClientCertificate
|
|
676
|
+
? serviceKeyStore.getClientCertificate(destination)
|
|
677
|
+
: null));
|
|
678
|
+
clientAuthentication = await (0, clientAuthentication_1.resolveClientAuthentication)(this.clientAuthentication, context);
|
|
679
|
+
client = await (0, clientAuthentication_1.clientIdentity)(context);
|
|
680
|
+
}
|
|
681
|
+
const computed = (0, bindingOf_1.destinationBinding)(authType, grant, stated, client);
|
|
682
|
+
// The strategy path: a resource neither side states matches.
|
|
683
|
+
const binding = clientAuthentication
|
|
684
|
+
? (0, binding_1.strategyBinding)(computed)
|
|
685
|
+
: computed;
|
|
686
|
+
const stored = grant === 'client_credentials'
|
|
687
|
+
? null
|
|
688
|
+
: await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
|
|
689
|
+
const secret = this.boundOrDiscarded(destination, stored, binding);
|
|
690
|
+
// The expiry is fixed when the result arrives, not when a retry
|
|
691
|
+
// finally writes it — on a copy; the outcome of the write is recorded
|
|
692
|
+
// against the result itself, which the provider also returns to the
|
|
693
|
+
// token API.
|
|
694
|
+
const onTokens = (result) => this.writer.submit(destination, {
|
|
695
|
+
result: { ...result, expiresAt: expiryOf(result) },
|
|
696
|
+
original: result,
|
|
697
|
+
binding,
|
|
698
|
+
carry: 'bound',
|
|
699
|
+
});
|
|
700
|
+
const common = {
|
|
701
|
+
destination,
|
|
702
|
+
client,
|
|
703
|
+
secret,
|
|
704
|
+
...(clientAuthentication ? { clientAuthentication } : {}),
|
|
705
|
+
logger: this.logger,
|
|
706
|
+
onTokens,
|
|
707
|
+
};
|
|
708
|
+
if ((0, destinations_1.isUaaGrant)(grant)) {
|
|
709
|
+
provider = (0, destinations_1.uaaProvider)({
|
|
710
|
+
...common,
|
|
711
|
+
grant,
|
|
712
|
+
authorization: this.authorization,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
else if ((0, destinations_1.isOidcGrant)(grant)) {
|
|
716
|
+
provider = (0, destinations_1.oidcProvider)({
|
|
717
|
+
...common,
|
|
718
|
+
grant,
|
|
719
|
+
means: stated,
|
|
720
|
+
oidcAuthorization: this.oidcAuthorization,
|
|
721
|
+
deviceCodePresenter: this.deviceCodePresenter,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
else if ((0, destinations_1.isSamlGrant)(grant)) {
|
|
725
|
+
provider = (0, destinations_1.samlProvider)({
|
|
726
|
+
...common,
|
|
727
|
+
grant,
|
|
728
|
+
means: stated,
|
|
729
|
+
authorization: this.authorization,
|
|
730
|
+
samlCookies: this.samlCookies,
|
|
731
|
+
assertionReplayStore: this.assertionReplayStore,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
// statedGrant admits only the allowed `authType` / `grantType` pairs.
|
|
736
|
+
throw new Error(`unreachable grant ${grant}`);
|
|
737
|
+
}
|
|
738
|
+
this.logger.debug(`[AuthBroker] Provider built for ${destination}`, {
|
|
739
|
+
authType,
|
|
740
|
+
grant,
|
|
741
|
+
seeded: !!secret,
|
|
742
|
+
});
|
|
743
|
+
return provider;
|
|
744
|
+
}
|
|
745
|
+
const secret = await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
|
|
746
|
+
const client = authType === 'jwt'
|
|
747
|
+
? await this.read(destination, 'client', () => serviceKeyStore.getAuthorizationConfig(destination))
|
|
748
|
+
: null;
|
|
749
|
+
provider = (0, destinations_1.handedOverProvider)(destination, authType, secret, (0, bindingOf_1.destinationBinding)(authType, 'none', stated, client));
|
|
750
|
+
}
|
|
751
|
+
this.logger.debug(`[AuthBroker] Provider built for ${destination}`, {
|
|
752
|
+
authType,
|
|
753
|
+
});
|
|
754
|
+
return provider;
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* The stored secret when it is bound to this destination's resource and
|
|
758
|
+
* issuer, else `null`: the provider is then built as with no session
|
|
759
|
+
* and logs in afresh — the refresh token is not spent either. The log line
|
|
760
|
+
* names the destination only: never a URI, never a token.
|
|
761
|
+
*/
|
|
762
|
+
boundOrDiscarded(destination, stored, binding) {
|
|
763
|
+
if (!stored || (0, binding_1.boundHere)(stored, binding))
|
|
764
|
+
return stored;
|
|
765
|
+
if (present(stored.authorizationToken) ||
|
|
766
|
+
present(stored.sessionCookies) ||
|
|
767
|
+
present(stored.refreshToken)) {
|
|
768
|
+
this.logger.warn(`[AuthBroker] ${destination}: secret bound to another resource, discarded`);
|
|
769
|
+
}
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Waits for the session writes still pending — each gets one more attempt —
|
|
774
|
+
* and rejects naming the destinations whose store still refuses. Call it on
|
|
775
|
+
* shutdown to know whether every token a provider obtained is stored; the
|
|
776
|
+
* broker keeps retrying after a rejection.
|
|
777
|
+
*/
|
|
778
|
+
flush() {
|
|
779
|
+
return this.writer.flush();
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* One write of the destination's session secret — and nothing else:
|
|
783
|
+
* `{ authorizationToken, expiresAt, refreshToken, issuedFor, issuedBy }` in
|
|
784
|
+
* one `saveSession` — or, for a `saml2_pure` result (`tokenType: 'saml'`),
|
|
785
|
+
* `{ sessionCookies, expiresAt, issuedFor, issuedBy }`: cookies are written
|
|
786
|
+
* as cookies, every other result as a token (`saml2_bearer` included). No means is ever written: not `serviceUrl`, not
|
|
787
|
+
* `authType`, not the client — they live in the key store, which the broker
|
|
788
|
+
* never writes.
|
|
789
|
+
*
|
|
790
|
+
* - `expiresAt`: fixed by `onTokens` when the result arrived — the result's
|
|
791
|
+
* own, else the `expiresIn` it reports counted from then (the rule the
|
|
792
|
+
* provider applies to its own cache).
|
|
793
|
+
* - `refreshToken`: the result's, else the one the session holds, read at
|
|
794
|
+
* write time, so a result without one does not erase the stored one — for
|
|
795
|
+
* a provider the broker built, only a stored one bound where this one is:
|
|
796
|
+
* a refresh token obtained for another resource or from another client is
|
|
797
|
+
* not carried into this secret. So too for a consumer's factory beside a
|
|
798
|
+
* `clientAuthentication` strategy, seeded only with a bound session. A
|
|
799
|
+
* consumer's provider without one was seeded with whatever the session
|
|
800
|
+
* held, so whatever it holds is carried (4.0.0).
|
|
801
|
+
* - `issuedFor` / `issuedBy`: the binding computed when the provider was
|
|
802
|
+
* built, each left out when the means lack its source — so the
|
|
803
|
+
* store clears it.
|
|
804
|
+
* - A destination the key store now states as `basic` or `snc` is not
|
|
805
|
+
* written: those obtain no session secret.
|
|
806
|
+
*/
|
|
807
|
+
async writeSecret(destination, { result, binding, carry }) {
|
|
808
|
+
const serviceKeyStore = this.serviceKeyStore;
|
|
809
|
+
const means = serviceKeyStore
|
|
810
|
+
? await this.read(destination, 'means', () => serviceKeyStore.getConnectionConfig(destination))
|
|
811
|
+
: null;
|
|
812
|
+
if (means?.authType === 'basic' || means?.authType === 'snc') {
|
|
813
|
+
this.logger.warn(`[AuthBroker] Not written: ${destination} is a ${means.authType} destination, which holds no session secret`);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
let secret;
|
|
817
|
+
if (result.tokenType === 'saml') {
|
|
818
|
+
// saml2_pure: the cookies are the credential, and SAML has no refresh
|
|
819
|
+
// token to carry.
|
|
820
|
+
secret = {
|
|
821
|
+
sessionCookies: result.authorizationToken,
|
|
822
|
+
expiresAt: result.expiresAt,
|
|
823
|
+
};
|
|
281
824
|
}
|
|
282
|
-
|
|
825
|
+
else {
|
|
826
|
+
const stored = result.refreshToken
|
|
827
|
+
? null
|
|
828
|
+
: await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
|
|
829
|
+
const storedRefreshToken = present(stored?.refreshToken) &&
|
|
830
|
+
(carry === 'any' || (0, binding_1.boundHere)(stored, binding))
|
|
831
|
+
? stored.refreshToken
|
|
832
|
+
: undefined;
|
|
833
|
+
secret = {
|
|
834
|
+
authorizationToken: result.authorizationToken,
|
|
835
|
+
expiresAt: result.expiresAt,
|
|
836
|
+
refreshToken: result.refreshToken || storedRefreshToken,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
if (binding.issuedFor !== undefined)
|
|
840
|
+
secret.issuedFor = binding.issuedFor;
|
|
841
|
+
if (binding.issuedBy !== undefined)
|
|
842
|
+
secret.issuedBy = binding.issuedBy;
|
|
843
|
+
await this.sessionStore.saveSession(destination, secret);
|
|
844
|
+
this.logger.info(`[AuthBroker] Session secret saved for ${destination}`, {
|
|
845
|
+
credential: secret.sessionCookies !== undefined ? 'cookies' : 'token',
|
|
846
|
+
hasRefreshToken: !!secret.refreshToken,
|
|
847
|
+
expiresAt: result.expiresAt,
|
|
848
|
+
});
|
|
283
849
|
}
|
|
284
850
|
/**
|
|
285
851
|
* An `ITokenRefresher` for one destination, for injection into a connection:
|