@mcp-abap-adt/auth-broker 4.0.0 → 5.0.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 +273 -1
- package/README.md +1028 -908
- package/dist/AuthBroker.d.ts +353 -99
- package/dist/AuthBroker.d.ts.map +1 -1
- package/dist/AuthBroker.js +806 -258
- package/dist/DestinationConfigError.d.ts +35 -6
- package/dist/DestinationConfigError.d.ts.map +1 -1
- package/dist/DestinationConfigError.js +51 -9
- package/dist/SessionWriter.d.ts +70 -33
- package/dist/SessionWriter.d.ts.map +1 -1
- package/dist/SessionWriter.js +118 -94
- package/dist/binding.d.ts +137 -64
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +330 -131
- package/dist/bindingOf.d.ts +33 -14
- package/dist/bindingOf.d.ts.map +1 -1
- package/dist/bindingOf.js +50 -20
- package/dist/buildIdentity.d.ts +137 -0
- package/dist/buildIdentity.d.ts.map +1 -0
- package/dist/buildIdentity.js +393 -0
- package/dist/clientAuthentication.d.ts +124 -0
- package/dist/clientAuthentication.d.ts.map +1 -0
- package/dist/clientAuthentication.js +218 -0
- package/dist/contractShape.d.ts +24 -0
- package/dist/contractShape.d.ts.map +1 -0
- package/dist/contractShape.js +24 -0
- package/dist/destinations.d.ts +174 -53
- package/dist/destinations.d.ts.map +1 -1
- package/dist/destinations.js +460 -150
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/quietLogger.d.ts +24 -0
- package/dist/quietLogger.d.ts.map +1 -0
- package/dist/quietLogger.js +63 -0
- package/package.json +9 -7
package/dist/AuthBroker.js
CHANGED
|
@@ -4,13 +4,20 @@
|
|
|
4
4
|
*
|
|
5
5
|
* `getProvider` builds the `IAuthProvider` a destination states, from the
|
|
6
6
|
* means its service key store holds and the secret its session store holds.
|
|
7
|
-
* Every token provider it builds
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Every token provider it builds renews through the consumer's `renewal` and
|
|
8
|
+
* writes what it obtains back to the session store through auth-providers'
|
|
9
|
+
* `refreshStatePersistence` — the secret alone, through one plain queue per
|
|
10
|
+
* destination (`SessionWriter`): a failed write stays pending until the
|
|
11
|
+
* destination's next write or `flush()`, and means what the consumer's
|
|
12
|
+
* `onWriteFailure` says; `flush()` reports what is still pending.
|
|
10
13
|
* The token API (`getToken`, `refreshToken`, `createTokenRefresher`) asks that
|
|
11
|
-
* same provider —
|
|
12
|
-
*
|
|
13
|
-
* path.
|
|
14
|
+
* same provider — the row path's — or, when the consumer gives one, the
|
|
15
|
+
* consumer's provider — the consumer path's, resolved, cached and bound on its
|
|
16
|
+
* own — whose every answer it writes through the same write path.
|
|
17
|
+
*
|
|
18
|
+
* A provider is never changed: every call re-reads what its path's
|
|
19
|
+
* provider was built from and builds a new one, starting with nothing, when
|
|
20
|
+
* anything of it changed (`buildIdentity.ts`).
|
|
14
21
|
*
|
|
15
22
|
* The broker orchestrates and nothing more. It resolves what the stores know
|
|
16
23
|
* about a destination, hands it to the provider, asks the provider for a token
|
|
@@ -21,18 +28,58 @@
|
|
|
21
28
|
*/
|
|
22
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
30
|
exports.AuthBroker = void 0;
|
|
31
|
+
const auth_errors_1 = require("@mcp-abap-adt/auth-errors");
|
|
24
32
|
const interfaces_auth_1 = require("@mcp-abap-adt/interfaces-auth");
|
|
25
33
|
const binding_1 = require("./binding");
|
|
26
34
|
const bindingOf_1 = require("./bindingOf");
|
|
35
|
+
const buildIdentity_1 = require("./buildIdentity");
|
|
36
|
+
const clientAuthentication_1 = require("./clientAuthentication");
|
|
37
|
+
const contractShape_1 = require("./contractShape");
|
|
27
38
|
const DestinationConfigError_1 = require("./DestinationConfigError");
|
|
28
39
|
const destinations_1 = require("./destinations");
|
|
40
|
+
const quietLogger_1 = require("./quietLogger");
|
|
29
41
|
const SessionWriter_1 = require("./SessionWriter");
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
42
|
+
const DROPPED = Object.freeze({ written: false });
|
|
43
|
+
/** `work`'s result or what it threw, as a frozen outcome; never rejects. */
|
|
44
|
+
async function outcomeOf(work) {
|
|
45
|
+
try {
|
|
46
|
+
return Object.freeze({ ok: true, value: await work() });
|
|
47
|
+
}
|
|
48
|
+
catch (thrown) {
|
|
49
|
+
return Object.freeze({ ok: false, thrown });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Joins `slot` with `start`'s work and the caller's signal, and unwraps the
|
|
54
|
+
* outcome outside `join`: the value, or the very value the work threw.
|
|
55
|
+
*/
|
|
56
|
+
async function joined(slot, work, signal) {
|
|
57
|
+
const outcome = await slot.join((attempt) => outcomeOf(() => work(attempt)), signal);
|
|
58
|
+
if (!outcome.ok)
|
|
59
|
+
throw outcome.thrown;
|
|
60
|
+
return outcome.value;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* One wait of one caller — on the write queue, or a store read outside a
|
|
64
|
+
* resolution — as a waiter of a slot of its own: the caller's signal
|
|
65
|
+
* releases that caller only, and the work runs on.
|
|
66
|
+
*/
|
|
67
|
+
function waitFor(work, signal) {
|
|
68
|
+
return joined((0, auth_errors_1.sharedAttempt)('persisting-tokens'), work, signal);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Rejects `aborted` when the caller's signal has aborted by now — after its
|
|
72
|
+
* last wait, before its success — through the same one implementation: a
|
|
73
|
+
* waiter that joins with an aborted signal is refused at once.
|
|
74
|
+
*/
|
|
75
|
+
async function stillWanted(signal) {
|
|
76
|
+
if (signal !== undefined)
|
|
77
|
+
await waitFor(async () => undefined, signal);
|
|
78
|
+
}
|
|
79
|
+
/** The provider a token API call is answered by, asked with the call's signal. */
|
|
80
|
+
function tokenOptions(signal) {
|
|
81
|
+
return signal === undefined ? [] : [{ signal }];
|
|
82
|
+
}
|
|
36
83
|
/** The session secret's fields on a connection config — never means. */
|
|
37
84
|
/** The session's side: the secret and what it is bound to. */
|
|
38
85
|
const SECRET_FIELDS = [
|
|
@@ -43,6 +90,33 @@ const SECRET_FIELDS = [
|
|
|
43
90
|
'issuedBy',
|
|
44
91
|
];
|
|
45
92
|
const isSecretField = (field) => SECRET_FIELDS.includes(field);
|
|
93
|
+
/**
|
|
94
|
+
* The connection means a strategy-path seed carries — the connection's own,
|
|
95
|
+
* none of them a secret. Everything else a store's connection read answers
|
|
96
|
+
* (credentials, grant data, fields outside the type) stays behind.
|
|
97
|
+
*/
|
|
98
|
+
const SEED_MEANS = [
|
|
99
|
+
'serviceUrl',
|
|
100
|
+
'sapClient',
|
|
101
|
+
'language',
|
|
102
|
+
'authType',
|
|
103
|
+
'grantType',
|
|
104
|
+
];
|
|
105
|
+
/**
|
|
106
|
+
* The token API factory's seed: the connection means from an allowlist
|
|
107
|
+
* (`SEED_MEANS`) and nothing else — never the session secret's own fields
|
|
108
|
+
* (`SECRET_FIELDS`), whatever the stored record says: the consumer path is
|
|
109
|
+
* never seeded. `serviceUrl` is the one resolved for the destination.
|
|
110
|
+
*/
|
|
111
|
+
function consumerSeed(connConfig, serviceUrl) {
|
|
112
|
+
const source = (connConfig ?? {});
|
|
113
|
+
const seed = {};
|
|
114
|
+
for (const field of SEED_MEANS) {
|
|
115
|
+
if (source[field] !== undefined)
|
|
116
|
+
seed[field] = source[field];
|
|
117
|
+
}
|
|
118
|
+
return { ...seed, serviceUrl };
|
|
119
|
+
}
|
|
46
120
|
/** When the result expires, in epoch ms: its own `expiresAt`, else `expiresIn` from now. */
|
|
47
121
|
function expiryOf(result) {
|
|
48
122
|
if (typeof result.expiresAt === 'number')
|
|
@@ -52,12 +126,68 @@ function expiryOf(result) {
|
|
|
52
126
|
}
|
|
53
127
|
return undefined;
|
|
54
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* The refresh token a build owns, after one reported refresh token: a
|
|
131
|
+
* string written makes it owned; `null` (a discard) writes `''` and makes it
|
|
132
|
+
* none; `undefined` writes the owned one, or `''` when the build owns none.
|
|
133
|
+
*/
|
|
134
|
+
function ownedAfter(owned, reported) {
|
|
135
|
+
if (present(reported))
|
|
136
|
+
return { written: reported, owned: reported };
|
|
137
|
+
if (reported === null)
|
|
138
|
+
return { written: '', owned: undefined };
|
|
139
|
+
return { written: owned ?? '', owned };
|
|
140
|
+
}
|
|
141
|
+
/** One map per path. */
|
|
142
|
+
function perPath() {
|
|
143
|
+
return { row: new Map(), consumer: new Map() };
|
|
144
|
+
}
|
|
55
145
|
/** A provider `getProvider` built that obtains tokens: a token provider. */
|
|
56
146
|
function obtainsTokens(provider) {
|
|
57
147
|
const candidate = provider;
|
|
58
148
|
return (typeof candidate.getTokens === 'function' &&
|
|
59
149
|
typeof candidate.refreshTokens === 'function');
|
|
60
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* The client the consumer path hands its factory and tells its strategy: the
|
|
153
|
+
* session's own client, else the key store's, through the same allowlist the
|
|
154
|
+
* strategy is told (`contextClient`) — with `refreshToken` as a key, never a
|
|
155
|
+
* value: no refresh token either client read carried reaches the factory.
|
|
156
|
+
*/
|
|
157
|
+
function consumerClient(read) {
|
|
158
|
+
const client = (0, clientAuthentication_1.contextClient)(read.sessionAuth ?? read.keyAuth);
|
|
159
|
+
return client
|
|
160
|
+
? (0, contractShape_1.asContract)({ ...client, refreshToken: undefined })
|
|
161
|
+
: null;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The consumer path's identity. A factory's: everything it is handed —
|
|
165
|
+
* the client, the seed — and what its binding is made of — the row, the
|
|
166
|
+
* resource. An instance's, which is handed nothing: the destination's
|
|
167
|
+
* identity it was first used for — the row, the resource, the issuer
|
|
168
|
+
* (`uaaUrl`, `oidcIssuerUrl`) and the client id, no secret.
|
|
169
|
+
*/
|
|
170
|
+
function consumerIdentity(inputs, instance) {
|
|
171
|
+
const { row, serviceUrl, sapClient, client } = inputs;
|
|
172
|
+
return instance
|
|
173
|
+
? {
|
|
174
|
+
row,
|
|
175
|
+
serviceUrl,
|
|
176
|
+
sapClient,
|
|
177
|
+
uaaUrl: client?.uaaUrl,
|
|
178
|
+
clientId: client?.uaaClientId,
|
|
179
|
+
oidcIssuerUrl: inputs.means?.oidcIssuerUrl,
|
|
180
|
+
}
|
|
181
|
+
: { row, serviceUrl, sapClient, client, seed: inputs.seed };
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The token API's refusal of a destination whose means changed under the
|
|
185
|
+
* consumer's instance: the instance cannot be rebuilt, and the identity of
|
|
186
|
+
* the credential it holds is unknown to the broker.
|
|
187
|
+
*/
|
|
188
|
+
function instanceRefusal(destination) {
|
|
189
|
+
return new DestinationConfigError_1.DestinationConfigError(destination, ['provider'], 'the destination’s means changed since the provider instance was first used for it');
|
|
190
|
+
}
|
|
61
191
|
/** A stored string that counts as present: `''` is none. */
|
|
62
192
|
function present(value) {
|
|
63
193
|
return typeof value === 'string' && value !== '';
|
|
@@ -69,13 +199,28 @@ function errorCode(error) {
|
|
|
69
199
|
}
|
|
70
200
|
return undefined;
|
|
71
201
|
}
|
|
72
|
-
/**
|
|
73
|
-
|
|
202
|
+
/**
|
|
203
|
+
* A result carrying a token, or the failure for one that carries none:
|
|
204
|
+
* `request-failed`, `token-source`, `no-access-token` — "the token source
|
|
205
|
+
* returned no access_token", minted by auth-errors.
|
|
206
|
+
*/
|
|
207
|
+
function checked(result) {
|
|
74
208
|
if (!result?.authorizationToken) {
|
|
75
|
-
throw new
|
|
209
|
+
throw new auth_errors_1.AuthProviderFailure(auth_errors_1.authError['request-failed']({
|
|
210
|
+
operation: 'token-source',
|
|
211
|
+
problem: 'no-access-token',
|
|
212
|
+
}));
|
|
76
213
|
}
|
|
77
214
|
return result;
|
|
78
215
|
}
|
|
216
|
+
/** A token write of the provider's persistence: the report's own facts. */
|
|
217
|
+
function persisted(tokens) {
|
|
218
|
+
return {
|
|
219
|
+
credential: tokens.authorizationToken,
|
|
220
|
+
cookies: tokens.tokenType === 'saml',
|
|
221
|
+
expiresAt: tokens.expiresAt,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
79
224
|
/**
|
|
80
225
|
* AuthBroker manages authentication tokens for destinations
|
|
81
226
|
*/
|
|
@@ -84,28 +229,37 @@ class AuthBroker {
|
|
|
84
229
|
serviceKeyStore;
|
|
85
230
|
sessionStore;
|
|
86
231
|
provider;
|
|
87
|
-
/**
|
|
88
|
-
|
|
232
|
+
/** Per (destination, path): the provider handed out while unchanged. */
|
|
233
|
+
resolvedRow = new Map();
|
|
234
|
+
resolvedConsumer = new Map();
|
|
89
235
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* build, set before its first read.
|
|
236
|
+
* Per (destination, path): the slot of shared resolutions — every
|
|
237
|
+
* caller of that path is a waiter of its attempt.
|
|
93
238
|
*/
|
|
94
|
-
|
|
239
|
+
rowSlots = new Map();
|
|
240
|
+
consumerSlots = new Map();
|
|
241
|
+
/** The broker-wide slot every `flush()` caller joins. */
|
|
242
|
+
flushSlot = (0, auth_errors_1.sharedAttempt)('persisting-tokens');
|
|
243
|
+
/** Per (destination, path): the last generation a build took. */
|
|
244
|
+
generations = perPath();
|
|
245
|
+
/** Per (destination, path): the newest generation that queued a write. */
|
|
246
|
+
newestWriter = perPath();
|
|
95
247
|
/**
|
|
96
|
-
* The
|
|
97
|
-
*
|
|
98
|
-
* for: the token API throws it to its caller, as 3.x did, while the broker
|
|
99
|
-
* keeps retrying. Dropped once a write of that result for that destination
|
|
100
|
-
* lands.
|
|
248
|
+
* The destinations whose means changed under the consumer's instance: the
|
|
249
|
+
* token API refuses them until a new broker.
|
|
101
250
|
*/
|
|
102
|
-
|
|
251
|
+
instanceRefused = new Set();
|
|
103
252
|
authorization;
|
|
104
253
|
oidcAuthorization;
|
|
105
254
|
deviceCodePresenter;
|
|
106
255
|
samlCookies;
|
|
107
256
|
assertionReplayStore;
|
|
108
|
-
|
|
257
|
+
clientAuthentication;
|
|
258
|
+
renewal;
|
|
259
|
+
onWriteFailure;
|
|
260
|
+
/** `config.authDebug === true`: what every token provider it builds is told. */
|
|
261
|
+
authDebug;
|
|
262
|
+
/** Every write of the session secret: one plain queue per destination. */
|
|
109
263
|
writer;
|
|
110
264
|
/**
|
|
111
265
|
* @param config Stores and the provider (instance or factory)
|
|
@@ -158,25 +312,15 @@ class AuthBroker {
|
|
|
158
312
|
this.deviceCodePresenter = config.deviceCodePresenter;
|
|
159
313
|
this.samlCookies = config.samlCookies;
|
|
160
314
|
this.assertionReplayStore = config.assertionReplayStore;
|
|
161
|
-
this.
|
|
162
|
-
this.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
let failures = this.failedWrites.get(bound.original);
|
|
171
|
-
if (!failures) {
|
|
172
|
-
failures = new Map();
|
|
173
|
-
this.failedWrites.set(bound.original, failures);
|
|
174
|
-
}
|
|
175
|
-
failures.set(destination, { error });
|
|
176
|
-
throw error;
|
|
177
|
-
}
|
|
178
|
-
this.failedWrites.get(bound.original)?.delete(destination);
|
|
179
|
-
}, this.logger);
|
|
315
|
+
this.clientAuthentication = config.clientAuthentication;
|
|
316
|
+
this.renewal = config.renewal;
|
|
317
|
+
this.onWriteFailure = config.onWriteFailure;
|
|
318
|
+
// Every line the broker writes, and every line of a provider it builds,
|
|
319
|
+
// goes through one guard: a consumer logger that throws or rejects
|
|
320
|
+
// changes no outcome.
|
|
321
|
+
this.logger = (0, quietLogger_1.quietLogger)(logger);
|
|
322
|
+
this.authDebug = config.authDebug === true;
|
|
323
|
+
this.writer = new SessionWriter_1.SessionWriter((destination, write) => this.writeSecret(destination, write), this.logger);
|
|
180
324
|
this.logger.debug('[AuthBroker] Broker initialized', {
|
|
181
325
|
hasServiceKeyStore: !!serviceKeyStore,
|
|
182
326
|
providerForm: provider === undefined
|
|
@@ -192,36 +336,83 @@ class AuthBroker {
|
|
|
192
336
|
*
|
|
193
337
|
* Without a consumer `provider`, the provider is the one `getProvider` hands
|
|
194
338
|
* out for the destination — the same instance, so one token and one renewal
|
|
195
|
-
* serve both — and what it obtains is written by its
|
|
339
|
+
* serve both — and what it obtains is written by its persistence. With one,
|
|
196
340
|
* the consumer's provider is asked and its answer written, as in 3.x — the
|
|
197
341
|
* secret alone.
|
|
198
342
|
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
343
|
+
* A provider's failure is relayed as the same object; a store's read
|
|
344
|
+
* failure reaches the caller as the store raised it. A write that did not
|
|
345
|
+
* land fails the call under `onWriteFailure: 'fail'` (`unknown`,
|
|
346
|
+
* `persisting-tokens`) — the token stands, and the write stays pending: the
|
|
347
|
+
* destination's next call retries it first and is refused while it fails —
|
|
348
|
+
* and is only logged under `'continue'`.
|
|
202
349
|
*
|
|
203
350
|
* @throws DestinationConfigError for a destination the key store states as
|
|
204
|
-
* `basic` or `snc`, before any provider is asked; without
|
|
205
|
-
* `
|
|
351
|
+
* `basic` or `snc`, before any provider is asked; without the
|
|
352
|
+
* `onWriteFailure` option; without a consumer `provider`, also for one
|
|
353
|
+
* whose provider obtains no token.
|
|
206
354
|
*/
|
|
207
|
-
async getToken(destination) {
|
|
208
|
-
return this.obtain(destination, 'getTokens');
|
|
355
|
+
async getToken(destination, options) {
|
|
356
|
+
return this.obtain(destination, 'getTokens', options?.signal);
|
|
209
357
|
}
|
|
210
358
|
/**
|
|
211
359
|
* A new token for the destination, never the cached one — for a caller whose
|
|
212
360
|
* token the server has just refused. Calls the provider's `refreshTokens()`;
|
|
213
361
|
* a renewal already in flight for the destination is joined, not repeated.
|
|
214
362
|
*/
|
|
215
|
-
async refreshToken(destination) {
|
|
216
|
-
return this.obtain(destination, 'refreshTokens');
|
|
363
|
+
async refreshToken(destination, options) {
|
|
364
|
+
return this.obtain(destination, 'refreshTokens', options?.signal);
|
|
217
365
|
}
|
|
218
|
-
|
|
219
|
-
|
|
366
|
+
/**
|
|
367
|
+
* The token API's one body. The call's signal releases this caller from
|
|
368
|
+
* every wait it has — the write checks, the means read, the resolution
|
|
369
|
+
* (whose waiter it is, never attaching), its own write — and is
|
|
370
|
+
* passed to the provider's `getTokens` / `refreshTokens`.
|
|
371
|
+
*/
|
|
372
|
+
async obtain(destination, method, signal) {
|
|
373
|
+
if (this.provider && !(0, destinations_1.isWriteFailurePolicy)(this.onWriteFailure)) {
|
|
374
|
+
// Every answer of the consumer's provider is written: what a failed
|
|
375
|
+
// write means is the consumer's to say. (Without one, getProvider's
|
|
376
|
+
// token row refuses the same.)
|
|
377
|
+
throw new DestinationConfigError_1.DestinationConfigError(destination, ['onWriteFailure'], 'the token API writes the session secret: say what a failed write means');
|
|
378
|
+
}
|
|
379
|
+
await this.settlePending(destination, signal);
|
|
380
|
+
// This call's reads: the means once, for the check and the resolution.
|
|
381
|
+
const reads = this.storeReads(destination);
|
|
382
|
+
const means = await waitFor(() => this.statedForTokens(destination, reads), signal);
|
|
220
383
|
const result = this.provider
|
|
221
|
-
? await this.obtainFromConsumer(destination, method,
|
|
222
|
-
: await this.obtainShared(destination, method, means);
|
|
384
|
+
? await this.obtainFromConsumer(destination, method, reads, signal)
|
|
385
|
+
: await this.obtainShared(destination, method, means, reads, signal);
|
|
386
|
+
// A write queued meanwhile — a discard of the destination's provider the
|
|
387
|
+
// store rejected, say — is caught here.
|
|
388
|
+
await this.settlePending(destination, signal);
|
|
389
|
+
// A signal that aborted after the last wait: never success.
|
|
390
|
+
await stillWanted(signal);
|
|
223
391
|
return result.authorizationToken;
|
|
224
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Under `onWriteFailure: 'fail'`, a call of the destination is refused while
|
|
395
|
+
* its last write is pending — not yet landed: failed, or still queued
|
|
396
|
+
* or in flight. Asked on entry and right before success: the call awaits
|
|
397
|
+
* every write of the destination queued before this moment, and a failed
|
|
398
|
+
* one that no later write replaced is written once more — the destination's
|
|
399
|
+
* next write, queued like any other. All landed: on entry the call goes on,
|
|
400
|
+
* at the end it returns its success. Still failing: it rejects (`unknown`,
|
|
401
|
+
* `persisting-tokens`). A write queued after the check is not this call's.
|
|
402
|
+
* Under `'continue'` nothing is asked.
|
|
403
|
+
*
|
|
404
|
+
* The one wait is `writer.retry(destination)`'s promise, a waiter of its
|
|
405
|
+
* own slot with the call's signal: an abort releases the caller
|
|
406
|
+
* (`aborted`), never with success, and the write runs on.
|
|
407
|
+
*/
|
|
408
|
+
async settlePending(destination, signal) {
|
|
409
|
+
if (this.onWriteFailure !== 'fail')
|
|
410
|
+
return;
|
|
411
|
+
const outcome = await waitFor(() => this.writer.retry(destination), signal);
|
|
412
|
+
if (!outcome.landed) {
|
|
413
|
+
throw new auth_errors_1.AuthProviderFailure((0, auth_errors_1.classify)(outcome.error, 'persisting-tokens'));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
225
416
|
/**
|
|
226
417
|
* The destination's means, read first: a destination the key store states
|
|
227
418
|
* as `basic` or `snc` holds no token, so the token API refuses it before any
|
|
@@ -229,10 +420,9 @@ class AuthBroker {
|
|
|
229
420
|
* one. A key store that states no `authType`, or no key store, is
|
|
230
421
|
* a 3.x setup, served as before.
|
|
231
422
|
*/
|
|
232
|
-
async statedForTokens(destination) {
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
? await this.read(destination, 'service key connection config', () => serviceKeyStore.getConnectionConfig(destination))
|
|
423
|
+
async statedForTokens(destination, reads) {
|
|
424
|
+
const means = this.serviceKeyStore
|
|
425
|
+
? (await reads.read('means'))
|
|
236
426
|
: null;
|
|
237
427
|
const authType = means?.authType;
|
|
238
428
|
if (authType === 'basic' || authType === 'snc') {
|
|
@@ -240,8 +430,11 @@ class AuthBroker {
|
|
|
240
430
|
}
|
|
241
431
|
return means;
|
|
242
432
|
}
|
|
243
|
-
/**
|
|
244
|
-
|
|
433
|
+
/**
|
|
434
|
+
* The token API on the row path's provider — the one `getProvider` hands
|
|
435
|
+
* out, resolved the same way, never through `getProvider` itself.
|
|
436
|
+
*/
|
|
437
|
+
async obtainShared(destination, method, means, reads, signal) {
|
|
245
438
|
if (!this.serviceKeyStore) {
|
|
246
439
|
throw new DestinationConfigError_1.DestinationConfigError(destination, ['provider', 'serviceKeyStore'], 'the token API needs the provider option, or a service key store stating a token grant');
|
|
247
440
|
}
|
|
@@ -249,129 +442,302 @@ class AuthBroker {
|
|
|
249
442
|
means.grantType === 'none') {
|
|
250
443
|
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');
|
|
251
444
|
}
|
|
252
|
-
|
|
445
|
+
// The row path's provider, through a waiter of its slot that never
|
|
446
|
+
// attaches: a token call can neither keep a later moment's login
|
|
447
|
+
// alive nor bound it.
|
|
448
|
+
const { provider } = await this.resolveRow(destination, signal, reads);
|
|
253
449
|
if (!obtainsTokens(provider)) {
|
|
254
450
|
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');
|
|
255
451
|
}
|
|
256
452
|
this.logger.debug(`[AuthBroker] ${method} for ${destination}`);
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
return result;
|
|
453
|
+
// The provider's failure — a write its persistence awaited included — is
|
|
454
|
+
// relayed as the same object.
|
|
455
|
+
return checked(await provider[method](...tokenOptions(signal)));
|
|
261
456
|
}
|
|
262
457
|
/**
|
|
263
|
-
* The token API on the consumer's provider
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
458
|
+
* The token API on the consumer's provider: resolved on the consumer path,
|
|
459
|
+
* every answer — a cache hit included — written, the secret alone, through
|
|
460
|
+
* the broker's one write path, with the binding fixed when the provider was
|
|
461
|
+
* taken into use.
|
|
267
462
|
*/
|
|
268
|
-
async obtainFromConsumer(destination, method,
|
|
269
|
-
const
|
|
270
|
-
const serviceUrl = await this.resolveServiceUrl(destination, connConfig);
|
|
271
|
-
const { provider, binding } = await this.consumerProviderFor(destination, serviceUrl, connConfig, means);
|
|
463
|
+
async obtainFromConsumer(destination, method, reads, signal) {
|
|
464
|
+
const { provider, binding, generation } = await this.resolveConsumer(destination, reads, signal);
|
|
272
465
|
this.logger.debug(`[AuthBroker] ${method} for ${destination}`);
|
|
273
|
-
const result = checked(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
466
|
+
const result = checked(await provider[method](...tokenOptions(signal)));
|
|
467
|
+
// The call's own write, queued whatever the caller does next, and awaited
|
|
468
|
+
// raced against its signal: an abort releases the caller
|
|
469
|
+
// (`aborted`, never success) and the write runs on.
|
|
470
|
+
const queued = this.submit('consumer', destination, generation, {
|
|
471
|
+
credential: result.authorizationToken,
|
|
472
|
+
cookies: result.tokenType === 'saml',
|
|
473
|
+
expiresAt: expiryOf(result),
|
|
474
|
+
// The result is authoritative: no refresh token in it writes `''` — the
|
|
475
|
+
// store merges, and the stored one is never carried.
|
|
476
|
+
refreshToken: present(result.refreshToken) ? result.refreshToken : '',
|
|
277
477
|
binding,
|
|
278
|
-
carry: 'any',
|
|
279
478
|
});
|
|
280
|
-
|
|
479
|
+
// Handled here too: a wait refused at once (the signal already aborted)
|
|
480
|
+
// never awaits it. It does not reject — the writer never does, and its
|
|
481
|
+
// log lines go through the quiet logger — but nothing here relies on it.
|
|
482
|
+
queued.catch(() => { });
|
|
483
|
+
const outcome = await waitFor(() => queued, signal);
|
|
484
|
+
if (outcome.written && !outcome.landed && this.onWriteFailure === 'fail') {
|
|
485
|
+
throw new auth_errors_1.AuthProviderFailure((0, auth_errors_1.classify)(outcome.error, 'persisting-tokens'));
|
|
486
|
+
}
|
|
281
487
|
return result;
|
|
282
488
|
}
|
|
283
|
-
/**
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
489
|
+
/**
|
|
490
|
+
* The slot of one (destination, path)'s resolutions: a caller of
|
|
491
|
+
* the path arriving while an attempt is active joins it; when every caller
|
|
492
|
+
* of an attempt has aborted, the attempt leaves the slot at once and the
|
|
493
|
+
* next caller starts afresh. A failed or aborted resolution is not kept.
|
|
494
|
+
*/
|
|
495
|
+
slotOf(slots, destination) {
|
|
496
|
+
let slot = slots.get(destination);
|
|
497
|
+
if (slot === undefined) {
|
|
498
|
+
slot = (0, auth_errors_1.sharedAttempt)('token-source');
|
|
499
|
+
slots.set(destination, slot);
|
|
288
500
|
}
|
|
501
|
+
return slot;
|
|
502
|
+
}
|
|
503
|
+
/** Whether a build of this path was ever committed for the destination. */
|
|
504
|
+
everBuilt(path, destination) {
|
|
505
|
+
return this.generations[path].has(destination);
|
|
506
|
+
}
|
|
507
|
+
/** A committed build's generation, from its path's counter. */
|
|
508
|
+
nextGeneration(path, destination) {
|
|
509
|
+
const generation = (this.generations[path].get(destination) ?? 0) + 1;
|
|
510
|
+
this.generations[path].set(destination, generation);
|
|
511
|
+
return generation;
|
|
289
512
|
}
|
|
290
513
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
514
|
+
* Queues one write of a build of `path`. A write of a build older than the
|
|
515
|
+
* newest build of the same path that has queued a write for the destination
|
|
516
|
+
* is dropped — never written, never pending; a build of the other
|
|
517
|
+
* path never retires this one. A dropped write is not a failed one: the
|
|
518
|
+
* retired provider's holder keeps it, and the destination's session
|
|
519
|
+
* is the newer build's.
|
|
297
520
|
*/
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
if (
|
|
301
|
-
|
|
521
|
+
async submit(path, destination, generation, write) {
|
|
522
|
+
const newest = this.newestWriter[path].get(destination) ?? 0;
|
|
523
|
+
if (generation < newest) {
|
|
524
|
+
this.logger.debug(`[AuthBroker] A replaced provider's session write for ${destination} dropped`);
|
|
525
|
+
return DROPPED;
|
|
302
526
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
527
|
+
this.newestWriter[path].set(destination, generation);
|
|
528
|
+
return Object.freeze({
|
|
529
|
+
written: true,
|
|
530
|
+
...(await this.writer.submit(destination, write)),
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
/** This call's store reads: each source read at most once. */
|
|
534
|
+
storeReads(destination) {
|
|
535
|
+
return new buildIdentity_1.StoreReads((name) => {
|
|
536
|
+
const serviceKeyStore = this.serviceKeyStore;
|
|
537
|
+
switch (name) {
|
|
538
|
+
case 'means':
|
|
539
|
+
return serviceKeyStore
|
|
540
|
+
? this.read(destination, 'means', () => serviceKeyStore.getConnectionConfig(destination))
|
|
541
|
+
: Promise.resolve(null);
|
|
542
|
+
case 'client':
|
|
543
|
+
return serviceKeyStore
|
|
544
|
+
? this.read(destination, 'client', () => serviceKeyStore.getAuthorizationConfig(destination))
|
|
545
|
+
: Promise.resolve(null);
|
|
546
|
+
case 'certificate':
|
|
547
|
+
return this.read(destination, 'client certificate', async () => serviceKeyStore?.getClientCertificate
|
|
548
|
+
? serviceKeyStore.getClientCertificate(destination)
|
|
549
|
+
: null);
|
|
550
|
+
case 'sessionConnection':
|
|
551
|
+
return this.read(destination, 'session connection config', () => this.sessionStore.getConnectionConfig(destination));
|
|
552
|
+
case 'sessionClient':
|
|
553
|
+
return this.read(destination, 'session authorization config', () => this.sessionStore.getAuthorizationConfig(destination));
|
|
313
554
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
555
|
+
}, (field) => new DestinationConfigError_1.DestinationConfigError(destination, [field], `the store answered ${field} in a shape the broker cannot take`));
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* The consumer path's provider for the destination: the cached one
|
|
559
|
+
* while everything it was built from is unchanged; else, for a factory, a
|
|
560
|
+
* new build — the factory called again, its provider starting with nothing
|
|
561
|
+
* — and, for an instance, which cannot be rebuilt, a refusal naming
|
|
562
|
+
* `provider` until a new broker.
|
|
563
|
+
*/
|
|
564
|
+
resolveConsumer(destination, reads, signal) {
|
|
565
|
+
const slot = this.slotOf(this.consumerSlots, destination);
|
|
566
|
+
return joined(slot, async (attempt) => {
|
|
567
|
+
const instance = typeof this.provider !== 'function';
|
|
568
|
+
if (this.instanceRefused.has(destination)) {
|
|
569
|
+
throw instanceRefusal(destination);
|
|
570
|
+
}
|
|
571
|
+
const cached = this.resolvedConsumer.get(destination);
|
|
572
|
+
if (cached) {
|
|
573
|
+
if (await cached.identity.unchanged(reads))
|
|
574
|
+
return cached;
|
|
575
|
+
// A doomed attempt changes nothing: every caller left while
|
|
576
|
+
// it re-read, and a fresh attempt may have committed meanwhile —
|
|
577
|
+
// the entry is no longer this attempt's to remove. A live attempt
|
|
578
|
+
// is the slot's only one, so the entry is still the one it read.
|
|
579
|
+
if (attempt.signal.aborted)
|
|
580
|
+
throw attempt.signal.reason;
|
|
581
|
+
this.resolvedConsumer.delete(destination);
|
|
582
|
+
if (instance) {
|
|
583
|
+
this.instanceRefused.add(destination);
|
|
584
|
+
throw instanceRefusal(destination);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const recorder = new buildIdentity_1.IdentityRecorder(reads);
|
|
588
|
+
const built = await this.buildConsumer(destination, recorder, attempt.signal);
|
|
589
|
+
// The commit, one step, only if the attempt was not aborted: a build
|
|
590
|
+
// every caller left is never cached nor handed out.
|
|
591
|
+
if (attempt.signal.aborted)
|
|
592
|
+
throw attempt.signal.reason;
|
|
593
|
+
const resolved = Object.freeze({
|
|
594
|
+
...built,
|
|
595
|
+
identity: recorder.seal(),
|
|
596
|
+
generation: this.nextGeneration('consumer', destination),
|
|
323
597
|
});
|
|
598
|
+
this.resolvedConsumer.set(destination, resolved);
|
|
599
|
+
return resolved;
|
|
600
|
+
}, signal);
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Takes the consumer's provider into use for the destination: a factory's
|
|
604
|
+
* build — handed the means and the client, never a stored secret — or an
|
|
605
|
+
* instance as given, handed nothing. The binding is fixed here, from the
|
|
606
|
+
* `serviceUrl` (the session's, else the key store's), the SAP client and
|
|
607
|
+
* the client of this build. Every store answer is read through `recorder`:
|
|
608
|
+
* what it read is the build's identity.
|
|
609
|
+
*/
|
|
610
|
+
async buildConsumer(destination, recorder, signal) {
|
|
611
|
+
const provider = this.provider;
|
|
612
|
+
const instance = typeof provider !== 'function';
|
|
613
|
+
// The identity is what this path takes from its stores — never what else
|
|
614
|
+
// the session store holds, which this broker's own writes change.
|
|
615
|
+
const inputs = await this.consumerInputs(destination, recorder.reads);
|
|
616
|
+
recorder.derived(consumerIdentity(inputs, instance), async (reads) => consumerIdentity(await this.consumerInputs(destination, reads), instance));
|
|
617
|
+
const { means, serviceUrl, sapClient, row, client, seed } = inputs;
|
|
618
|
+
if (typeof provider !== 'function') {
|
|
324
619
|
return {
|
|
325
|
-
provider
|
|
326
|
-
binding: (0, binding_1.consumerBinding)(serviceUrl, sapClient,
|
|
620
|
+
provider,
|
|
621
|
+
binding: (0, binding_1.consumerBinding)(row, serviceUrl, sapClient, null),
|
|
327
622
|
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
623
|
+
}
|
|
624
|
+
// Never seeded: the factory is handed the means and the client
|
|
625
|
+
// only — no stored token, cookies, expiry or refresh token, in any
|
|
626
|
+
// argument, whatever the stored session's record says. The broker
|
|
627
|
+
// cannot know what the factory composes from what it is handed.
|
|
628
|
+
const strategic = await this.consumerClientAuthentication(destination, means, client, recorder, signal);
|
|
629
|
+
let built;
|
|
630
|
+
let binding;
|
|
631
|
+
if (strategic) {
|
|
632
|
+
// The strategy's answer and the client identity, resolved before the
|
|
633
|
+
// factory; the factory called inside the same guard: a throw is fixed
|
|
634
|
+
// words, nothing of the thrown value, no cause. The binding is only
|
|
635
|
+
// ever written, never compared: this path seeds from nothing.
|
|
636
|
+
binding = (0, binding_1.consumerBinding)(row, serviceUrl, sapClient, strategic.identity);
|
|
637
|
+
const fourth = {
|
|
638
|
+
clientAuthentication: strategic.clientAuthentication,
|
|
639
|
+
...(strategic.identity
|
|
640
|
+
? {
|
|
641
|
+
uaaUrl: strategic.identity.uaaUrl,
|
|
642
|
+
clientId: strategic.identity.uaaClientId,
|
|
643
|
+
}
|
|
644
|
+
: {}),
|
|
645
|
+
};
|
|
646
|
+
try {
|
|
647
|
+
built = provider(destination, client, seed, fourth);
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
throw new DestinationConfigError_1.DestinationConfigError(destination, ['provider', 'clientAuthentication'], 'the token provider factory failed beside the clientAuthentication strategy');
|
|
333
651
|
}
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
binding = (0, binding_1.consumerBinding)(row, serviceUrl, sapClient, client);
|
|
655
|
+
built = provider(destination, client, seed);
|
|
656
|
+
}
|
|
657
|
+
this.logger.debug(`[AuthBroker] Provider built for ${destination}`, {
|
|
658
|
+
hasCredentials: !!client,
|
|
334
659
|
});
|
|
335
|
-
return
|
|
660
|
+
return { provider: built, binding };
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* The token API's strategy path: with a `clientAuthentication` strategy and a
|
|
664
|
+
* destination stating a grant whose client authenticates, the strategy's
|
|
665
|
+
* answer — resolved inside the guard — and the client identity: the secret
|
|
666
|
+
* client, else the certificate client's, read through the context's memoised
|
|
667
|
+
* `readCertificate`. `null` without a strategy (4.0.0: nothing
|
|
668
|
+
* certificate-related is asked) and for a grant that authenticates no
|
|
669
|
+
* client (`saml2_pure`, `none`), as `getProvider` decides.
|
|
670
|
+
*
|
|
671
|
+
* The strategy is told the grant, so a destination that states none is
|
|
672
|
+
* refused before it is called rather than built without it. (An instance
|
|
673
|
+
* given as `provider` is the consumer's own composition: the strategy
|
|
674
|
+
* applies only to what the broker builds, so it is not used there.)
|
|
675
|
+
*/
|
|
676
|
+
async consumerClientAuthentication(destination, means, client, recorder, signal) {
|
|
677
|
+
const strategy = this.clientAuthentication;
|
|
678
|
+
if (!strategy) {
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
// basic and snc never get here: the token API refuses them first
|
|
682
|
+
// (`statedForTokens`).
|
|
683
|
+
if (!means || (means.authType !== 'jwt' && means.authType !== 'saml')) {
|
|
684
|
+
throw new DestinationConfigError_1.DestinationConfigError(destination, ['authType', 'grantType'], 'a clientAuthentication strategy is told the grant: the destination must state its authType and grantType');
|
|
685
|
+
}
|
|
686
|
+
const grant = (0, destinations_1.statedGrant)(destination, means.authType, means);
|
|
687
|
+
if (grant === 'saml2_pure' || grant === 'none') {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
const context = (0, clientAuthentication_1.clientAuthenticationContext)(destination, grant, client, () => recorder.read('certificate'), signal);
|
|
691
|
+
const clientAuthentication = await (0, clientAuthentication_1.resolveClientAuthentication)(strategy, context);
|
|
692
|
+
return { clientAuthentication, identity: await (0, clientAuthentication_1.clientIdentity)(context) };
|
|
336
693
|
}
|
|
337
694
|
/**
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
695
|
+
* What a consumer's factory's credentials are composed from, read in the
|
|
696
|
+
* 3.x order: the session's authorization config; only when it has none, the
|
|
697
|
+
* key store's client. The stored session is not read: nothing of it is
|
|
698
|
+
* handed to the factory. A session store of auth-stores 3
|
|
341
699
|
* answers no client, so the key store's is what is found.
|
|
342
700
|
*/
|
|
343
|
-
async
|
|
344
|
-
const sessionAuth = await
|
|
701
|
+
async readAuthorization(reads) {
|
|
702
|
+
const sessionAuth = (await reads.read('sessionClient'));
|
|
345
703
|
if (sessionAuth) {
|
|
346
|
-
return sessionAuth;
|
|
704
|
+
return { sessionAuth, keyAuth: null };
|
|
347
705
|
}
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
? session.refreshToken
|
|
351
|
-
: undefined;
|
|
352
|
-
const serviceKeyStore = this.serviceKeyStore;
|
|
353
|
-
const keyAuth = serviceKeyStore
|
|
354
|
-
? await this.read(destination, 'service key authorization config', () => serviceKeyStore.getAuthorizationConfig(destination))
|
|
706
|
+
const keyAuth = this.serviceKeyStore
|
|
707
|
+
? (await reads.read('client'))
|
|
355
708
|
: null;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
709
|
+
return { sessionAuth: null, keyAuth };
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* What the consumer path takes from its stores for one destination, read
|
|
713
|
+
* from `reads` — the same function for a build and for every later call's
|
|
714
|
+
* comparison: the means, the `serviceUrl` (the session's, else the key
|
|
715
|
+
* store's), the SAP client, the row, the client through its allowlist, and
|
|
716
|
+
* the factory's seed of allowlisted connection means.
|
|
717
|
+
*/
|
|
718
|
+
async consumerInputs(destination, reads) {
|
|
719
|
+
const means = this.serviceKeyStore
|
|
720
|
+
? (await reads.read('means'))
|
|
721
|
+
: null;
|
|
722
|
+
const connConfig = (await reads.read('sessionConnection'));
|
|
723
|
+
const serviceUrl = this.serviceUrlOf(destination, connConfig, means);
|
|
359
724
|
return {
|
|
360
|
-
|
|
361
|
-
|
|
725
|
+
means,
|
|
726
|
+
serviceUrl,
|
|
727
|
+
sapClient: present(connConfig?.sapClient)
|
|
728
|
+
? connConfig.sapClient
|
|
729
|
+
: means?.sapClient,
|
|
730
|
+
row: (0, bindingOf_1.consumerRow)(means),
|
|
731
|
+
client: consumerClient(await this.readAuthorization(reads)),
|
|
732
|
+
seed: consumerSeed(connConfig, serviceUrl),
|
|
362
733
|
};
|
|
363
734
|
}
|
|
364
735
|
/**
|
|
365
736
|
* `serviceUrl` for a consumer's provider, in the 3.x order: the session's,
|
|
366
737
|
* else the key store's, else an error before the provider is asked.
|
|
367
738
|
*/
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const serviceKeyStore = this.serviceKeyStore;
|
|
371
|
-
if (!serviceUrl && serviceKeyStore) {
|
|
372
|
-
const keyConn = await this.read(destination, 'service key connection config', () => serviceKeyStore.getConnectionConfig(destination));
|
|
373
|
-
serviceUrl = keyConn?.serviceUrl;
|
|
374
|
-
}
|
|
739
|
+
serviceUrlOf(destination, connConfig, means) {
|
|
740
|
+
const serviceUrl = connConfig?.serviceUrl || means?.serviceUrl;
|
|
375
741
|
if (!serviceUrl) {
|
|
376
742
|
throw new Error(`Session for destination "${destination}" is missing required field 'serviceUrl'. ` +
|
|
377
743
|
`SessionStore must contain initial session with serviceUrl${this.serviceKeyStore ? ' or serviceKeyStore must contain serviceUrl' : ''}.`);
|
|
@@ -459,39 +825,108 @@ class AuthBroker {
|
|
|
459
825
|
* built from the means the service key store holds and the secret the
|
|
460
826
|
* session store holds.
|
|
461
827
|
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
828
|
+
* A provider is never changed: every call re-reads what the
|
|
829
|
+
* destination's provider was built from — the means, the client, and the
|
|
830
|
+
* certificate client when the build read it — and answers the cached
|
|
831
|
+
* provider while all of it is unchanged. When anything changed, by a single
|
|
832
|
+
* character, it builds a new provider, which starts with nothing — no token,
|
|
833
|
+
* no refresh token, nothing of the old provider or of a session written
|
|
834
|
+
* under other means — and never hands the old one out again; whoever holds
|
|
835
|
+
* it keeps it, and its writes are dropped once the new one has written.
|
|
836
|
+
* Concurrent calls share one resolution; a resolution that threw is tried
|
|
837
|
+
* again on the next call.
|
|
464
838
|
*
|
|
465
839
|
* A token provider (the UAA, OIDC and SAML grants) is seeded from the
|
|
466
|
-
* session
|
|
467
|
-
*
|
|
468
|
-
*
|
|
840
|
+
* session only at its destination's first build in this broker, and only
|
|
841
|
+
* when the stored secret is bound to exactly this build's means (fully
|
|
842
|
+
* stated); it writes every token it obtains — at `prepare()`, on expiry, or
|
|
843
|
+
* in `rejected()` after a 401 — back to the session store before it answers
|
|
844
|
+
* (see `flush()`).
|
|
845
|
+
*
|
|
846
|
+
* `options.signal` releases this caller from every wait (`aborted`), and is
|
|
847
|
+
* attached to the token or SNC provider answered — built or cached — so a
|
|
848
|
+
* login it starts later is aborted once every session holding it has gone
|
|
849
|
+
*. Without a signal nothing is attached.
|
|
469
850
|
*
|
|
470
851
|
* @throws DestinationConfigError when the destination lacks what its type
|
|
471
852
|
* needs — naming the fields or options, never a value.
|
|
472
853
|
*/
|
|
473
|
-
getProvider(destination) {
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
//
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
854
|
+
async getProvider(destination, options) {
|
|
855
|
+
const signal = options?.signal;
|
|
856
|
+
await this.settlePending(destination, signal);
|
|
857
|
+
const { provider, parties } = await this.resolveRow(destination, signal);
|
|
858
|
+
await this.settlePending(destination, signal);
|
|
859
|
+
// A signal that aborted after the last wait: never success.
|
|
860
|
+
await stillWanted(signal);
|
|
861
|
+
// Attached after the resolution, to the provider answered — built or
|
|
862
|
+
// from the cache — so a login it starts later in a moment is aborted once
|
|
863
|
+
// every session holding it has gone. Nothing kept to detach: a
|
|
864
|
+
// session ends by aborting its signal.
|
|
865
|
+
if (signal !== undefined)
|
|
866
|
+
parties?.attach(signal);
|
|
867
|
+
return provider;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* The row path's provider for the destination: the cached one while what
|
|
871
|
+
* its build read is unchanged, else a new build, committed with the
|
|
872
|
+
* next generation of the row path.
|
|
873
|
+
*/
|
|
874
|
+
resolveRow(destination, signal, reads = this.storeReads(destination)) {
|
|
875
|
+
const slot = this.slotOf(this.rowSlots, destination);
|
|
876
|
+
return joined(slot, async (attempt) => {
|
|
877
|
+
const cached = this.resolvedRow.get(destination);
|
|
878
|
+
if (cached) {
|
|
879
|
+
if (await cached.identity.unchanged(reads))
|
|
880
|
+
return cached;
|
|
881
|
+
// A doomed attempt changes nothing: every caller left while
|
|
882
|
+
// it re-read, and a fresh attempt may have committed meanwhile —
|
|
883
|
+
// the entry is no longer this attempt's to remove. A live attempt
|
|
884
|
+
// is the slot's only one, so the entry is still the one it read.
|
|
885
|
+
if (attempt.signal.aborted)
|
|
886
|
+
throw attempt.signal.reason;
|
|
887
|
+
// Changed: never handed out again.
|
|
888
|
+
this.resolvedRow.delete(destination);
|
|
485
889
|
}
|
|
486
|
-
|
|
487
|
-
|
|
890
|
+
const recorder = new buildIdentity_1.IdentityRecorder(reads);
|
|
891
|
+
const commit = { generation: 0 };
|
|
892
|
+
const { provider, parties } = await this.build(destination, recorder, {
|
|
893
|
+
// Only a first build may start from the store; a replacement starts
|
|
894
|
+
// with nothing.
|
|
895
|
+
seeded: !this.everBuilt('row', destination),
|
|
896
|
+
commit,
|
|
897
|
+
signal: attempt.signal,
|
|
898
|
+
});
|
|
899
|
+
// The commit — the cache set, the generation taken — is one step that
|
|
900
|
+
// runs only if the attempt was not aborted: a build every caller left
|
|
901
|
+
// is never cached, never handed out, and, never asked for a token,
|
|
902
|
+
// writes nothing.
|
|
903
|
+
if (attempt.signal.aborted)
|
|
904
|
+
throw attempt.signal.reason;
|
|
905
|
+
commit.generation = this.nextGeneration('row', destination);
|
|
906
|
+
const resolved = Object.freeze({
|
|
907
|
+
provider,
|
|
908
|
+
parties,
|
|
909
|
+
identity: recorder.seal(),
|
|
910
|
+
generation: commit.generation,
|
|
911
|
+
});
|
|
912
|
+
this.resolvedRow.set(destination, resolved);
|
|
913
|
+
return resolved;
|
|
914
|
+
}, signal);
|
|
488
915
|
}
|
|
489
|
-
|
|
916
|
+
/**
|
|
917
|
+
* One build of the destination's row. Every store answer it reads goes
|
|
918
|
+
* through `recorder` — what it read is its identity. `seeded`: whether the
|
|
919
|
+
* build may start from the stored session; `commit`: the generation its
|
|
920
|
+
* writes carry, set when the build is committed; `signal`: the build's
|
|
921
|
+
* attempt, handed to the `clientAuthentication` strategy. Answers
|
|
922
|
+
* the provider and the row it stated.
|
|
923
|
+
*/
|
|
924
|
+
async build(destination, recorder, { seeded, commit, signal, }) {
|
|
490
925
|
const serviceKeyStore = this.serviceKeyStore;
|
|
491
926
|
if (!serviceKeyStore) {
|
|
492
927
|
throw new DestinationConfigError_1.DestinationConfigError(destination, ['serviceKeyStore'], 'getProvider reads the means from a service key store, and none was given');
|
|
493
928
|
}
|
|
494
|
-
const means = await
|
|
929
|
+
const means = await recorder.read('means');
|
|
495
930
|
const authType = (0, destinations_1.statedAuthType)(destination, means);
|
|
496
931
|
// statedAuthType refuses a destination without means.
|
|
497
932
|
const stated = means;
|
|
@@ -500,7 +935,11 @@ class AuthBroker {
|
|
|
500
935
|
provider = (0, destinations_1.basicProvider)(destination, stated);
|
|
501
936
|
}
|
|
502
937
|
else if (authType === 'snc') {
|
|
503
|
-
|
|
938
|
+
const snc = (0, destinations_1.sncProvider)(destination, stated, this.logger);
|
|
939
|
+
this.logger.debug(`[AuthBroker] Provider built for ${destination}`, {
|
|
940
|
+
authType,
|
|
941
|
+
});
|
|
942
|
+
return { provider: snc, parties: snc };
|
|
504
943
|
}
|
|
505
944
|
else {
|
|
506
945
|
const grant = (0, destinations_1.statedGrant)(destination, authType, stated);
|
|
@@ -509,40 +948,100 @@ class AuthBroker {
|
|
|
509
948
|
// saml2_pure), what a secret for this destination is bound to
|
|
510
949
|
// — written with every secret, and required of a stored one
|
|
511
950
|
// before it seeds — and the stored secret when it is.
|
|
512
|
-
const
|
|
951
|
+
const secretClient = grant === 'saml2_pure'
|
|
513
952
|
? null
|
|
514
|
-
: await
|
|
515
|
-
|
|
516
|
-
|
|
953
|
+
: await recorder.read('client');
|
|
954
|
+
// The consumer's renewal and write policy have no default: a row
|
|
955
|
+
// without either is refused — with everything else it lacks, in one
|
|
956
|
+
// error — before any collaborator, the clientAuthentication strategy
|
|
957
|
+
// included, is called.
|
|
958
|
+
if ((0, destinations_1.optionsLacking)({
|
|
959
|
+
renewal: this.renewal,
|
|
960
|
+
onWriteFailure: this.onWriteFailure,
|
|
961
|
+
}).length > 0) {
|
|
962
|
+
throw this.rowRefusal(destination, grant, stated, secretClient);
|
|
963
|
+
}
|
|
964
|
+
// The consumer's client authentication, resolved inside the guard
|
|
965
|
+
// before any provider exists: a strategy that throws leaves nothing
|
|
966
|
+
// built and nothing cached. Then the client identity the row and the
|
|
967
|
+
// binding take — the secret client, else the certificate client's —
|
|
968
|
+
// so the binding and the seed are chosen from it. Without a strategy
|
|
969
|
+
// nothing of this runs: 4.0.0's client, nothing certificate-related.
|
|
970
|
+
let client = secretClient;
|
|
971
|
+
let clientAuthentication;
|
|
972
|
+
// The certificate client, when the build read it: its certUrl and
|
|
973
|
+
// public certificate go into the binding (never its key).
|
|
974
|
+
let certificateRead;
|
|
975
|
+
if (this.clientAuthentication && grant !== 'saml2_pure') {
|
|
976
|
+
const context = (0, clientAuthentication_1.clientAuthenticationContext)(destination, grant, secretClient, () => {
|
|
977
|
+
certificateRead =
|
|
978
|
+
recorder.read('certificate');
|
|
979
|
+
return certificateRead;
|
|
980
|
+
}, signal);
|
|
981
|
+
clientAuthentication = await (0, clientAuthentication_1.resolveClientAuthentication)(this.clientAuthentication, context);
|
|
982
|
+
client = await (0, clientAuthentication_1.clientIdentity)(context);
|
|
983
|
+
}
|
|
984
|
+
// A read that failed — and that a consumer's strategy caught — read
|
|
985
|
+
// no certificate client.
|
|
986
|
+
const certificate = certificateRead
|
|
987
|
+
? await certificateRead.catch(() => null)
|
|
988
|
+
: null;
|
|
989
|
+
// Its certificate is hashed into the binding: one that is no PEM
|
|
990
|
+
// string is refused, never hashed as if the build had read none.
|
|
991
|
+
if (certificate && !present(certificate.certificate)) {
|
|
992
|
+
throw new DestinationConfigError_1.DestinationConfigError(destination, ['clientAuthentication'], 'the client certificate the key store answered holds no certificate');
|
|
993
|
+
}
|
|
994
|
+
const computed = (0, bindingOf_1.destinationBinding)(authType, grant, stated, client, certificate);
|
|
995
|
+
// The strategy path: a resource neither side states matches.
|
|
996
|
+
const binding = clientAuthentication
|
|
997
|
+
? (0, binding_1.strategyBinding)(computed)
|
|
998
|
+
: computed;
|
|
999
|
+
// Only a first build reads the session: a replacement for changed
|
|
1000
|
+
// means starts with nothing. client_credentials takes no seed.
|
|
1001
|
+
const stored = grant === 'client_credentials' || !seeded
|
|
517
1002
|
? null
|
|
518
1003
|
: await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
|
|
519
1004
|
const secret = this.boundOrDiscarded(destination, stored, binding);
|
|
520
|
-
// The
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
1005
|
+
// The refresh token this build owns: the one the builder handed its
|
|
1006
|
+
// provider from the checked session (`RowBuild`), then whatever each
|
|
1007
|
+
// of its writes made of it — never one read from the store at write
|
|
1008
|
+
// time. Set before the provider can write.
|
|
1009
|
+
let owned;
|
|
1010
|
+
// The provider's persistence writes through here: resolved when the
|
|
1011
|
+
// write landed, rejected with the store's error when it did not —
|
|
1012
|
+
// refreshStatePersistence then does what onWriteFailure says.
|
|
1013
|
+
const write = async (tokens) => {
|
|
1014
|
+
const next = ownedAfter(owned, tokens.refreshToken);
|
|
1015
|
+
owned = next.owned;
|
|
1016
|
+
const outcome = await this.submit('row', destination, commit.generation, {
|
|
1017
|
+
...persisted(tokens),
|
|
1018
|
+
refreshToken: next.written,
|
|
1019
|
+
binding,
|
|
1020
|
+
});
|
|
1021
|
+
if (outcome.written && !outcome.landed)
|
|
1022
|
+
throw outcome.error;
|
|
1023
|
+
};
|
|
530
1024
|
const common = {
|
|
531
1025
|
destination,
|
|
532
1026
|
client,
|
|
533
1027
|
secret,
|
|
1028
|
+
...(clientAuthentication ? { clientAuthentication } : {}),
|
|
1029
|
+
renewal: this.renewal,
|
|
1030
|
+
onWriteFailure: this.onWriteFailure,
|
|
1031
|
+
write,
|
|
534
1032
|
logger: this.logger,
|
|
535
|
-
|
|
1033
|
+
authDebug: this.authDebug,
|
|
536
1034
|
};
|
|
1035
|
+
let built;
|
|
537
1036
|
if ((0, destinations_1.isUaaGrant)(grant)) {
|
|
538
|
-
|
|
1037
|
+
built = (0, destinations_1.uaaProvider)({
|
|
539
1038
|
...common,
|
|
540
1039
|
grant,
|
|
541
1040
|
authorization: this.authorization,
|
|
542
1041
|
});
|
|
543
1042
|
}
|
|
544
1043
|
else if ((0, destinations_1.isOidcGrant)(grant)) {
|
|
545
|
-
|
|
1044
|
+
built = (0, destinations_1.oidcProvider)({
|
|
546
1045
|
...common,
|
|
547
1046
|
grant,
|
|
548
1047
|
means: stated,
|
|
@@ -551,7 +1050,7 @@ class AuthBroker {
|
|
|
551
1050
|
});
|
|
552
1051
|
}
|
|
553
1052
|
else if ((0, destinations_1.isSamlGrant)(grant)) {
|
|
554
|
-
|
|
1053
|
+
built = (0, destinations_1.samlProvider)({
|
|
555
1054
|
...common,
|
|
556
1055
|
grant,
|
|
557
1056
|
means: stated,
|
|
@@ -564,29 +1063,77 @@ class AuthBroker {
|
|
|
564
1063
|
// statedGrant admits only the allowed `authType` / `grantType` pairs.
|
|
565
1064
|
throw new Error(`unreachable grant ${grant}`);
|
|
566
1065
|
}
|
|
1066
|
+
owned = built.seededRefreshToken;
|
|
1067
|
+
const tokenProvider = built.provider;
|
|
567
1068
|
this.logger.debug(`[AuthBroker] Provider built for ${destination}`, {
|
|
568
1069
|
authType,
|
|
569
1070
|
grant,
|
|
570
1071
|
seeded: !!secret,
|
|
571
1072
|
});
|
|
572
|
-
return provider;
|
|
1073
|
+
return { provider: tokenProvider, parties: tokenProvider };
|
|
573
1074
|
}
|
|
574
1075
|
const secret = await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
|
|
575
1076
|
const client = authType === 'jwt'
|
|
576
|
-
? await
|
|
1077
|
+
? await recorder.read('client')
|
|
577
1078
|
: null;
|
|
578
1079
|
provider = (0, destinations_1.handedOverProvider)(destination, authType, secret, (0, bindingOf_1.destinationBinding)(authType, 'none', stated, client));
|
|
579
1080
|
}
|
|
580
1081
|
this.logger.debug(`[AuthBroker] Provider built for ${destination}`, {
|
|
581
1082
|
authType,
|
|
582
1083
|
});
|
|
583
|
-
return provider;
|
|
1084
|
+
return { provider, parties: undefined };
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* A token row's refusal when the consumer's `renewal` or `onWriteFailure`
|
|
1088
|
+
* is missing — named beside every other field and option the row lacks, in
|
|
1089
|
+
* one error — decided before any collaborator is called. With a
|
|
1090
|
+
* clientAuthentication strategy and no secret client, the client's fields
|
|
1091
|
+
* are not judged: the strategy would tell the certificate client's.
|
|
1092
|
+
*/
|
|
1093
|
+
rowRefusal(destination, grant, means, secretClient) {
|
|
1094
|
+
const clientAuthenticated = !!this.clientAuthentication && grant !== 'saml2_pure';
|
|
1095
|
+
const client = clientAuthenticated && !secretClient ? undefined : secretClient;
|
|
1096
|
+
const check = {
|
|
1097
|
+
destination,
|
|
1098
|
+
client,
|
|
1099
|
+
clientAuthenticated,
|
|
1100
|
+
renewal: this.renewal,
|
|
1101
|
+
onWriteFailure: this.onWriteFailure,
|
|
1102
|
+
};
|
|
1103
|
+
const refusal = (0, destinations_1.isUaaGrant)(grant)
|
|
1104
|
+
? (0, destinations_1.uaaRefusal)({ ...check, grant, authorization: this.authorization })
|
|
1105
|
+
: (0, destinations_1.isOidcGrant)(grant)
|
|
1106
|
+
? (0, destinations_1.oidcRefusal)({
|
|
1107
|
+
...check,
|
|
1108
|
+
grant,
|
|
1109
|
+
means,
|
|
1110
|
+
oidcAuthorization: this.oidcAuthorization,
|
|
1111
|
+
deviceCodePresenter: this.deviceCodePresenter,
|
|
1112
|
+
})
|
|
1113
|
+
: (0, destinations_1.samlRefusal)({
|
|
1114
|
+
...check,
|
|
1115
|
+
grant,
|
|
1116
|
+
means,
|
|
1117
|
+
authorization: this.authorization,
|
|
1118
|
+
samlCookies: this.samlCookies,
|
|
1119
|
+
assertionReplayStore: this.assertionReplayStore,
|
|
1120
|
+
});
|
|
1121
|
+
return (refusal ??
|
|
1122
|
+
new DestinationConfigError_1.DestinationConfigError(destination, (0, destinations_1.optionsLacking)(check), `a destination with grantType ${grant} lacks what its grant needs`));
|
|
584
1123
|
}
|
|
585
1124
|
/**
|
|
586
1125
|
* The stored secret when it is bound to this destination's resource and
|
|
587
1126
|
* issuer, else `null`: the provider is then built as with no session
|
|
588
|
-
* and logs in afresh — the refresh token is not spent either.
|
|
589
|
-
*
|
|
1127
|
+
* and logs in afresh — the refresh token is not spent either.
|
|
1128
|
+
*
|
|
1129
|
+
* A stored credential not taken is logged in fixed words naming the
|
|
1130
|
+
* destination only — never a URI, a record or a token: a `warn` when the
|
|
1131
|
+
* binding could ever be seeded (`couldBeSeeded`) — the stored secret is not
|
|
1132
|
+
* recorded under the current means: another resource, row, client,
|
|
1133
|
+
* endpoint or trust, a 4.x record, or none — and a `debug` line when it
|
|
1134
|
+
* never could — a row never seeded by design (`token_exchange`), means
|
|
1135
|
+
* lacking what the record needs, or no `serviceUrl` — since that holds on
|
|
1136
|
+
* every start and nothing was discarded by a change.
|
|
590
1137
|
*/
|
|
591
1138
|
boundOrDiscarded(destination, stored, binding) {
|
|
592
1139
|
if (!stored || (0, binding_1.boundHere)(stored, binding))
|
|
@@ -594,44 +1141,55 @@ class AuthBroker {
|
|
|
594
1141
|
if (present(stored.authorizationToken) ||
|
|
595
1142
|
present(stored.sessionCookies) ||
|
|
596
1143
|
present(stored.refreshToken)) {
|
|
597
|
-
|
|
1144
|
+
if ((0, binding_1.couldBeSeeded)(binding)) {
|
|
1145
|
+
this.logger.warn(`[AuthBroker] ${destination}: the stored session secret is not recorded as issued under the destination's current means; not used, the provider obtains a new one`);
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
this.logger.debug(`[AuthBroker] ${destination}: the destination's means do not state everything a session secret is bound to; a stored one is never used`);
|
|
1149
|
+
}
|
|
598
1150
|
}
|
|
599
1151
|
return null;
|
|
600
1152
|
}
|
|
601
1153
|
/**
|
|
602
|
-
* Waits for
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
1154
|
+
* Waits for every session write queued so far and gives each pending one —
|
|
1155
|
+
* a write that did not land — one more attempt; rejects naming the
|
|
1156
|
+
* destinations whose store still refuses (an `AggregateError` of
|
|
1157
|
+
* `SessionWriteFailure`s). Nothing retries a pending write on its own: only
|
|
1158
|
+
* the destination's next write or `flush()` does, so call it on shutdown to
|
|
1159
|
+
* know whether every token a provider obtained is stored.
|
|
1160
|
+
*
|
|
1161
|
+
* Every caller joins one broker-wide slot: its signal releases that
|
|
1162
|
+
* caller alone (`aborted`); the attempts run on, and a write still failing
|
|
1163
|
+
* stays pending. The `AggregateError` is rethrown as the same object to
|
|
1164
|
+
* every caller.
|
|
606
1165
|
*/
|
|
607
|
-
flush() {
|
|
608
|
-
return this.writer.flush();
|
|
1166
|
+
flush(options) {
|
|
1167
|
+
return joined(this.flushSlot, () => this.writer.flush(), options?.signal);
|
|
609
1168
|
}
|
|
610
1169
|
/**
|
|
611
|
-
* One write of the destination's session secret — and nothing else
|
|
612
|
-
* `
|
|
613
|
-
*
|
|
614
|
-
* `{ sessionCookies, expiresAt, issuedFor, issuedBy }`: cookies are written
|
|
615
|
-
* as cookies, every other result as a token (`saml2_bearer` included). No means is ever written: not `serviceUrl`, not
|
|
616
|
-
* `authType`, not the client — they live in the key store, which the broker
|
|
617
|
-
* never writes.
|
|
1170
|
+
* One write of the destination's session secret — and nothing else — in one
|
|
1171
|
+
* `saveSession`, built from the write alone: the store merges, so every
|
|
1172
|
+
* field that must not survive from an earlier write is stated.
|
|
618
1173
|
*
|
|
619
|
-
* -
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
1174
|
+
* - A credential: `{ authorizationToken, expiresAt, refreshToken, issuedFor,
|
|
1175
|
+
* issuedBy }`, or for `saml2_pure`'s cookies (`tokenType: 'saml'`)
|
|
1176
|
+
* `{ sessionCookies, expiresAt, refreshToken: '', issuedFor, issuedBy }` —
|
|
1177
|
+
* SAML has no refresh token, and one stored beside earlier cookies or a
|
|
1178
|
+
* token is not this credential's.
|
|
1179
|
+
* - `refreshToken`: decided when the write was submitted — the build's own,
|
|
1180
|
+
* or `''`, the store's clearing operation. The store is never read here.
|
|
1181
|
+
* - `issuedFor` / `issuedBy`: the binding fixed when the provider was built,
|
|
1182
|
+
* `issuedFor` written as `''` when the means lack its source — never left
|
|
1183
|
+
* out, so no earlier binding survives beside a new credential.
|
|
1184
|
+
* - A write that holds no credential (a refresh token discarded before any
|
|
1185
|
+
* was held) writes only `refreshToken: ''`: the stored credential keeps
|
|
1186
|
+
* its own binding — never re-labelled — and loses its refresh token.
|
|
1187
|
+
* - `expiresAt`: fixed when the credential arrived.
|
|
631
1188
|
* - A destination the key store now states as `basic` or `snc` is not
|
|
632
1189
|
* written: those obtain no session secret.
|
|
633
1190
|
*/
|
|
634
|
-
async writeSecret(destination,
|
|
1191
|
+
async writeSecret(destination, write) {
|
|
1192
|
+
const { binding } = write;
|
|
635
1193
|
const serviceKeyStore = this.serviceKeyStore;
|
|
636
1194
|
const means = serviceKeyStore
|
|
637
1195
|
? await this.read(destination, 'means', () => serviceKeyStore.getConnectionConfig(destination))
|
|
@@ -640,49 +1198,39 @@ class AuthBroker {
|
|
|
640
1198
|
this.logger.warn(`[AuthBroker] Not written: ${destination} is a ${means.authType} destination, which holds no session secret`);
|
|
641
1199
|
return;
|
|
642
1200
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
secret = {
|
|
648
|
-
sessionCookies: result.authorizationToken,
|
|
649
|
-
expiresAt: result.expiresAt,
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
else {
|
|
653
|
-
const stored = result.refreshToken
|
|
654
|
-
? null
|
|
655
|
-
: await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
|
|
656
|
-
const storedRefreshToken = present(stored?.refreshToken) &&
|
|
657
|
-
(carry === 'any' || (0, binding_1.boundHere)(stored, binding))
|
|
658
|
-
? stored.refreshToken
|
|
659
|
-
: undefined;
|
|
660
|
-
secret = {
|
|
661
|
-
authorizationToken: result.authorizationToken,
|
|
662
|
-
expiresAt: result.expiresAt,
|
|
663
|
-
refreshToken: result.refreshToken || storedRefreshToken,
|
|
664
|
-
};
|
|
1201
|
+
if (write.credential === '') {
|
|
1202
|
+
await this.sessionStore.saveSession(destination, (0, contractShape_1.asContract)({ refreshToken: '' }));
|
|
1203
|
+
this.logger.info(`[AuthBroker] Refresh token cleared for ${destination}`);
|
|
1204
|
+
return;
|
|
665
1205
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
1206
|
+
const secret = (0, contractShape_1.asContract)({
|
|
1207
|
+
...(write.cookies
|
|
1208
|
+
? { sessionCookies: write.credential }
|
|
1209
|
+
: { authorizationToken: write.credential }),
|
|
1210
|
+
expiresAt: write.expiresAt,
|
|
1211
|
+
// saml2_pure: the cookies are the credential, and SAML has none.
|
|
1212
|
+
refreshToken: write.cookies ? '' : write.refreshToken,
|
|
1213
|
+
issuedFor: binding.issuedFor ?? '',
|
|
1214
|
+
issuedBy: binding.issuedBy,
|
|
1215
|
+
});
|
|
670
1216
|
await this.sessionStore.saveSession(destination, secret);
|
|
671
1217
|
this.logger.info(`[AuthBroker] Session secret saved for ${destination}`, {
|
|
672
|
-
credential:
|
|
673
|
-
hasRefreshToken:
|
|
674
|
-
expiresAt:
|
|
1218
|
+
credential: write.cookies ? 'cookies' : 'token',
|
|
1219
|
+
hasRefreshToken: present(secret.refreshToken),
|
|
1220
|
+
expiresAt: write.expiresAt,
|
|
675
1221
|
});
|
|
676
1222
|
}
|
|
677
1223
|
/**
|
|
678
1224
|
* An `ITokenRefresher` for one destination, for injection into a connection:
|
|
679
1225
|
* `getToken()` is the broker's `getToken`, `refreshToken()` its forced
|
|
680
|
-
* `refreshToken
|
|
1226
|
+
* `refreshToken` — each call a waiter with `options.signal`, so a
|
|
1227
|
+
* refresher held by a session ends with it.
|
|
681
1228
|
*/
|
|
682
|
-
createTokenRefresher(destination) {
|
|
1229
|
+
createTokenRefresher(destination, options) {
|
|
1230
|
+
const signal = options?.signal;
|
|
683
1231
|
return {
|
|
684
|
-
getToken: () => this.getToken(destination),
|
|
685
|
-
refreshToken: () => this.refreshToken(destination),
|
|
1232
|
+
getToken: () => this.getToken(destination, { signal }),
|
|
1233
|
+
refreshToken: () => this.refreshToken(destination, { signal }),
|
|
686
1234
|
};
|
|
687
1235
|
}
|
|
688
1236
|
}
|