@mcp-abap-adt/auth-broker 3.0.4 → 4.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +448 -1
  2. package/README.md +723 -533
  3. package/dist/AuthBroker.d.ts +178 -29
  4. package/dist/AuthBroker.d.ts.map +1 -1
  5. package/dist/AuthBroker.js +494 -101
  6. package/dist/DestinationConfigError.d.ts +27 -0
  7. package/dist/DestinationConfigError.d.ts.map +1 -0
  8. package/dist/DestinationConfigError.js +36 -0
  9. package/dist/SessionWriter.d.ts +54 -0
  10. package/dist/SessionWriter.d.ts.map +1 -0
  11. package/dist/SessionWriter.js +135 -0
  12. package/dist/binding.d.ts +95 -0
  13. package/dist/binding.d.ts.map +1 -0
  14. package/dist/binding.js +207 -0
  15. package/dist/bindingOf.d.ts +36 -0
  16. package/dist/bindingOf.d.ts.map +1 -0
  17. package/dist/bindingOf.js +53 -0
  18. package/dist/destinations.d.ts +139 -0
  19. package/dist/destinations.d.ts.map +1 -0
  20. package/dist/destinations.js +429 -0
  21. package/dist/index.d.ts +7 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +11 -7
  24. package/dist/providers/ITokenProvider.d.ts +2 -1
  25. package/dist/providers/ITokenProvider.d.ts.map +1 -1
  26. package/dist/providers/index.d.ts +2 -2
  27. package/dist/providers/index.js +2 -2
  28. package/dist/stores/index.d.ts +2 -2
  29. package/dist/stores/index.js +2 -2
  30. package/dist/stores/interfaces.d.ts +4 -3
  31. package/dist/stores/interfaces.d.ts.map +1 -1
  32. package/dist/stores/interfaces.js +2 -2
  33. package/dist/types.d.ts +5 -2
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js +3 -1
  36. package/package.json +25 -45
  37. package/CONTRIBUTORS.md +0 -33
  38. package/dist/__tests__/helpers/configHelpers.d.ts +0 -59
  39. package/dist/__tests__/helpers/configHelpers.d.ts.map +0 -1
  40. package/dist/__tests__/helpers/configHelpers.js +0 -206
  41. package/dist/__tests__/helpers/netHelpers.d.ts +0 -3
  42. package/dist/__tests__/helpers/netHelpers.d.ts.map +0 -1
  43. package/dist/__tests__/helpers/netHelpers.js +0 -63
  44. package/dist/__tests__/helpers/testLogger.d.ts +0 -7
  45. package/dist/__tests__/helpers/testLogger.d.ts.map +0 -1
  46. package/dist/__tests__/helpers/testLogger.js +0 -54
  47. package/dist/bin/mcp-auth.js +0 -752
  48. package/dist/bin/mcp-sso.js +0 -825
  49. package/dist/bin/mcpSsoConfig.js +0 -566
  50. package/dist/bin/samlMetadata.js +0 -175
  51. package/dist/bin/workDir.js +0 -79
@@ -1,6 +1,16 @@
1
1
  "use strict";
2
2
  /**
3
- * AuthBroker: tokens for a destination, from a provider, kept in a session store.
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,46 @@
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 DestinationConfigError_1 = require("./DestinationConfigError");
28
+ const destinations_1 = require("./destinations");
29
+ const SessionWriter_1 = require("./SessionWriter");
15
30
  const noOpLogger = {
16
31
  info: () => { },
17
32
  error: () => { },
18
33
  warn: () => { },
19
34
  debug: () => { },
20
35
  };
36
+ /** The session secret's fields on a connection config — never means. */
37
+ /** The session's side: the secret and what it is bound to. */
38
+ const SECRET_FIELDS = [
39
+ 'authorizationToken',
40
+ 'sessionCookies',
41
+ 'expiresAt',
42
+ 'issuedFor',
43
+ 'issuedBy',
44
+ ];
45
+ const isSecretField = (field) => SECRET_FIELDS.includes(field);
46
+ /** When the result expires, in epoch ms: its own `expiresAt`, else `expiresIn` from now. */
47
+ function expiryOf(result) {
48
+ if (typeof result.expiresAt === 'number')
49
+ return result.expiresAt;
50
+ if (typeof result.expiresIn === 'number') {
51
+ return Date.now() + Math.round(result.expiresIn * 1000);
52
+ }
53
+ return undefined;
54
+ }
55
+ /** A provider `getProvider` built that obtains tokens: a token provider. */
56
+ function obtainsTokens(provider) {
57
+ const candidate = provider;
58
+ return (typeof candidate.getTokens === 'function' &&
59
+ typeof candidate.refreshTokens === 'function');
60
+ }
61
+ /** A stored string that counts as present: `''` is none. */
62
+ function present(value) {
63
+ return typeof value === 'string' && value !== '';
64
+ }
21
65
  function errorCode(error) {
22
66
  if (error !== null && typeof error === 'object' && 'code' in error) {
23
67
  const code = error.code;
@@ -25,6 +69,13 @@ function errorCode(error) {
25
69
  }
26
70
  return undefined;
27
71
  }
72
+ /** A result carrying a token, or the 3.x error for one that carries none. */
73
+ function checked(destination, result) {
74
+ if (!result?.authorizationToken) {
75
+ throw new Error(`Token provider did not return authorization token for destination "${destination}"`);
76
+ }
77
+ return result;
78
+ }
28
79
  /**
29
80
  * AuthBroker manages authentication tokens for destinations
30
81
  */
@@ -33,7 +84,29 @@ class AuthBroker {
33
84
  serviceKeyStore;
34
85
  sessionStore;
35
86
  provider;
36
- providers = new Map();
87
+ /** A consumer factory's providers: the promise of the build, as `built`. */
88
+ consumerBuilt = new Map();
89
+ /**
90
+ * One cache per destination, shared by `getProvider` and — when the
91
+ * consumer gives no provider — the token API: the promise of the
92
+ * build, set before its first read.
93
+ */
94
+ built = new Map();
95
+ /**
96
+ * The write that failed for a result, keyed on the result the provider
97
+ * handed over and returns, then on the destination it was written
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.
101
+ */
102
+ failedWrites = new WeakMap();
103
+ authorization;
104
+ oidcAuthorization;
105
+ deviceCodePresenter;
106
+ samlCookies;
107
+ assertionReplayStore;
108
+ /** getProvider's writes of the session secret, retried on their own. */
109
+ writer;
37
110
  /**
38
111
  * @param config Stores and the provider (instance or factory)
39
112
  * @param logger Optional logger. Nothing the broker logs contains a token.
@@ -46,9 +119,6 @@ class AuthBroker {
46
119
  if (!sessionStore) {
47
120
  throw new Error('AuthBroker: sessionStore is required');
48
121
  }
49
- if (!provider) {
50
- throw new Error('AuthBroker: provider is required');
51
- }
52
122
  for (const method of [
53
123
  'getAuthorizationConfig',
54
124
  'getConnectionConfig',
@@ -61,7 +131,7 @@ class AuthBroker {
61
131
  throw new Error(`AuthBroker: sessionStore.${method} must be a function`);
62
132
  }
63
133
  }
64
- if (typeof provider !== 'function') {
134
+ if (provider && typeof provider !== 'function') {
65
135
  if (typeof provider.getTokens !== 'function') {
66
136
  throw new Error('AuthBroker: provider.getTokens must be a function');
67
137
  }
@@ -83,69 +153,192 @@ class AuthBroker {
83
153
  this.sessionStore = sessionStore;
84
154
  this.serviceKeyStore = serviceKeyStore;
85
155
  this.provider = provider;
156
+ this.authorization = config.authorization;
157
+ this.oidcAuthorization = config.oidcAuthorization;
158
+ this.deviceCodePresenter = config.deviceCodePresenter;
159
+ this.samlCookies = config.samlCookies;
160
+ this.assertionReplayStore = config.assertionReplayStore;
86
161
  this.logger = logger ?? noOpLogger;
162
+ this.writer = new SessionWriter_1.SessionWriter(async (destination, bound) => {
163
+ try {
164
+ await this.writeSecret(destination, bound);
165
+ }
166
+ catch (error) {
167
+ // Per destination: one result object may be answered for several
168
+ // (an instance serves every destination), and a write that lands for
169
+ // one says nothing about another's.
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);
87
180
  this.logger.debug('[AuthBroker] Broker initialized', {
88
181
  hasServiceKeyStore: !!serviceKeyStore,
89
- providerForm: typeof provider === 'function' ? 'factory' : 'instance',
182
+ providerForm: provider === undefined
183
+ ? 'none'
184
+ : typeof provider === 'function'
185
+ ? 'factory'
186
+ : 'instance',
90
187
  });
91
188
  }
92
189
  /**
93
190
  * A token for the destination: the provider's current one, which it refreshes
94
191
  * or obtains by login when it judges the cached one unusable.
95
192
  *
96
- * The result is written to the session store. Errors from the provider
97
- * (its typed errors included) propagate unchanged.
193
+ * Without a consumer `provider`, the provider is the one `getProvider` hands
194
+ * out for the destination — the same instance, so one token and one renewal
195
+ * serve both — and what it obtains is written by its `onTokens`. With one,
196
+ * the consumer's provider is asked and its answer written, as in 3.x — the
197
+ * secret alone.
198
+ *
199
+ * Errors from the provider (its typed errors included) propagate unchanged;
200
+ * so does a store failure, a failed write of the token included — the token
201
+ * stands and the broker keeps retrying the write.
202
+ *
203
+ * @throws DestinationConfigError for a destination the key store states as
204
+ * `basic` or `snc`, before any provider is asked; without a consumer
205
+ * `provider`, also for one whose provider obtains no token.
98
206
  */
99
207
  async getToken(destination) {
100
208
  return this.obtain(destination, 'getTokens');
101
209
  }
102
210
  /**
103
211
  * 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
- * writes the result to the session store and returns it.
212
+ * token the server has just refused. Calls the provider's `refreshTokens()`;
213
+ * a renewal already in flight for the destination is joined, not repeated.
106
214
  */
107
215
  async refreshToken(destination) {
108
216
  return this.obtain(destination, 'refreshTokens');
109
217
  }
110
218
  async obtain(destination, method) {
219
+ const means = await this.statedForTokens(destination);
220
+ const result = this.provider
221
+ ? await this.obtainFromConsumer(destination, method, means)
222
+ : await this.obtainShared(destination, method, means);
223
+ return result.authorizationToken;
224
+ }
225
+ /**
226
+ * The destination's means, read first: a destination the key store states
227
+ * as `basic` or `snc` holds no token, so the token API refuses it before any
228
+ * provider is asked — it would write a token over a credential that is not
229
+ * one. A key store that states no `authType`, or no key store, is
230
+ * a 3.x setup, served as before.
231
+ */
232
+ async statedForTokens(destination) {
233
+ const serviceKeyStore = this.serviceKeyStore;
234
+ const means = serviceKeyStore
235
+ ? await this.read(destination, 'service key connection config', () => serviceKeyStore.getConnectionConfig(destination))
236
+ : null;
237
+ const authType = means?.authType;
238
+ if (authType === 'basic' || authType === 'snc') {
239
+ throw new DestinationConfigError_1.DestinationConfigError(destination, ['authType'], `the token API serves no ${authType} destination: it holds no token`);
240
+ }
241
+ return means;
242
+ }
243
+ /** The token API on `getProvider`'s provider. */
244
+ async obtainShared(destination, method, means) {
245
+ if (!this.serviceKeyStore) {
246
+ 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
+ }
248
+ if ((means?.authType === 'jwt' || means?.authType === 'saml') &&
249
+ means.grantType === 'none') {
250
+ 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
+ }
252
+ const provider = await this.getProvider(destination);
253
+ if (!obtainsTokens(provider)) {
254
+ 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
+ }
256
+ this.logger.debug(`[AuthBroker] ${method} for ${destination}`);
257
+ const result = checked(destination, await provider[method]());
258
+ // onTokens wrote it, or failed to: the failure is this caller's too.
259
+ this.throwFailedWrite(destination, result);
260
+ return result;
261
+ }
262
+ /**
263
+ * The token API on the consumer's provider, as 3.x: the session read first,
264
+ * then the key store, for `serviceUrl` and the client; every answer — a
265
+ * cache hit included — written, the secret alone, through the broker's one
266
+ * write path.
267
+ */
268
+ async obtainFromConsumer(destination, method, means) {
111
269
  const connConfig = await this.read(destination, 'session connection config', () => this.sessionStore.getConnectionConfig(destination));
112
270
  const serviceUrl = await this.resolveServiceUrl(destination, connConfig);
113
- const provider = await this.providerFor(destination, serviceUrl, connConfig);
271
+ const { provider, binding } = await this.consumerProviderFor(destination, serviceUrl, connConfig, means);
114
272
  this.logger.debug(`[AuthBroker] ${method} for ${destination}`);
115
- const result = await provider[method]();
116
- if (!result?.authorizationToken) {
117
- throw new Error(`Token provider did not return authorization token for destination "${destination}"`);
118
- }
119
- await this.persist(destination, serviceUrl, connConfig, result);
120
- return result.authorizationToken;
273
+ const result = checked(destination, await provider[method]());
274
+ await this.writer.submit(destination, {
275
+ result: { ...result, expiresAt: expiryOf(result) },
276
+ original: result,
277
+ binding,
278
+ carry: 'any',
279
+ });
280
+ this.throwFailedWrite(destination, result);
281
+ return result;
121
282
  }
122
- async providerFor(destination, serviceUrl, connConfig) {
123
- if (typeof this.provider !== 'function') {
124
- return this.provider;
283
+ /** The failure recorded for this result's write here, thrown as the store raised it. */
284
+ throwFailedWrite(destination, result) {
285
+ const failed = this.failedWrites.get(result)?.get(destination);
286
+ if (failed) {
287
+ throw failed.error;
125
288
  }
126
- const existing = this.providers.get(destination);
127
- if (existing) {
128
- return existing;
289
+ }
290
+ /**
291
+ * The consumer's provider for the destination, cached per destination as a
292
+ * promise set before its first read and dropped when it throws, so
293
+ * the next call builds again: a factory's build — handed the client resolved
294
+ * then — or an instance as given, for every destination, handed no client.
295
+ * Either way the binding is fixed here, from the `serviceUrl`, SAP client and
296
+ * client of this first call, and kept with the provider.
297
+ */
298
+ consumerProviderFor(destination, serviceUrl, connConfig, means) {
299
+ const cached = this.consumerBuilt.get(destination);
300
+ if (cached) {
301
+ return cached;
129
302
  }
130
- const authConfig = await this.resolveAuthorizationConfig(destination);
131
- const built = this.provider(destination, authConfig, {
132
- ...(connConfig ?? {}),
133
- serviceUrl,
303
+ const provider = this.provider;
304
+ const sapClient = present(connConfig?.sapClient)
305
+ ? connConfig.sapClient
306
+ : means?.sapClient;
307
+ const build = Promise.resolve().then(async () => {
308
+ if (typeof provider !== 'function') {
309
+ return {
310
+ provider,
311
+ binding: (0, binding_1.consumerBinding)(serviceUrl, sapClient, null),
312
+ };
313
+ }
314
+ const client = await this.resolveAuthorizationConfig(destination);
315
+ const built = provider(destination, client, {
316
+ ...(connConfig ?? {}),
317
+ serviceUrl,
318
+ });
319
+ this.logger.debug(`[AuthBroker] Provider built for ${destination}`, {
320
+ hasCredentials: !!client,
321
+ hasRefreshToken: !!client?.refreshToken,
322
+ hasStoredToken: !!(connConfig?.authorizationToken || connConfig?.sessionCookies),
323
+ });
324
+ return {
325
+ provider: built,
326
+ binding: (0, binding_1.consumerBinding)(serviceUrl, sapClient, client),
327
+ };
134
328
  });
135
- this.providers.set(destination, built);
136
- this.logger.debug(`[AuthBroker] Provider built for ${destination}`, {
137
- hasCredentials: !!authConfig,
138
- hasRefreshToken: !!authConfig?.refreshToken,
139
- hasStoredToken: !!(connConfig?.authorizationToken || connConfig?.sessionCookies),
329
+ this.consumerBuilt.set(destination, build);
330
+ build.catch(() => {
331
+ if (this.consumerBuilt.get(destination) === build) {
332
+ this.consumerBuilt.delete(destination);
333
+ }
140
334
  });
141
- return built;
335
+ return build;
142
336
  }
143
337
  /**
144
- * The credentials the provider is built with: the session's own when it holds
145
- * them, else the service key's, carrying the refresh token the session
146
- * stored. The session keeps a refresh token without credentials when the
147
- * credentials came from the service key, since the broker does not copy the
148
- * client secret into it; `loadSession` is where such a token is read.
338
+ * The credentials a consumer's factory is built with: the session's own when
339
+ * it holds them, else the service key's, carrying the refresh token the
340
+ * session stored — the 3.x order. A session store of auth-stores 3
341
+ * answers no client, so the key store's is what is found.
149
342
  */
150
343
  async resolveAuthorizationConfig(destination) {
151
344
  const sessionAuth = await this.read(destination, 'session authorization config', () => this.sessionStore.getAuthorizationConfig(destination));
@@ -168,6 +361,10 @@ class AuthBroker {
168
361
  refreshToken: storedRefreshToken ?? keyAuth.refreshToken,
169
362
  };
170
363
  }
364
+ /**
365
+ * `serviceUrl` for a consumer's provider, in the 3.x order: the session's,
366
+ * else the key store's, else an error before the provider is asked.
367
+ */
171
368
  async resolveServiceUrl(destination, connConfig) {
172
369
  let serviceUrl = connConfig?.serviceUrl;
173
370
  const serviceKeyStore = this.serviceKeyStore;
@@ -181,53 +378,6 @@ class AuthBroker {
181
378
  }
182
379
  return serviceUrl;
183
380
  }
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
381
  /**
232
382
  * A store read where absence is an answer and anything else is not.
233
383
  *
@@ -252,34 +402,277 @@ class AuthBroker {
252
402
  }
253
403
  }
254
404
  /**
255
- * Authorization configuration for the destination: the session's, else the
256
- * service key's, else null.
405
+ * The destination's client: the service key store's `uaaUrl`, `uaaClientId`
406
+ * and `uaaClientSecret`, with the refresh token the session stores laid over
407
+ * them; `null` when the key store has no client. A client a session store
408
+ * answers is not read, nor a refresh token a key store answers.
257
409
  */
258
410
  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
411
  const serviceKeyStore = this.serviceKeyStore;
264
- if (!serviceKeyStore) {
412
+ const client = serviceKeyStore
413
+ ? await this.read(destination, 'service key authorization config', () => serviceKeyStore.getAuthorizationConfig(destination))
414
+ : null;
415
+ if (!client) {
265
416
  return null;
266
417
  }
267
- return this.read(destination, 'service key authorization config', () => serviceKeyStore.getAuthorizationConfig(destination));
418
+ const secret = await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
419
+ const composed = {
420
+ uaaUrl: client.uaaUrl,
421
+ uaaClientId: client.uaaClientId,
422
+ uaaClientSecret: client.uaaClientSecret,
423
+ };
424
+ if (secret?.refreshToken !== undefined) {
425
+ composed.refreshToken = secret.refreshToken;
426
+ }
427
+ return composed;
268
428
  }
269
429
  /**
270
- * Connection configuration for the destination: the session's, else the
271
- * service key's (which has URLs but no token), else null.
430
+ * The destination's connection config: the service key store's means with
431
+ * the session's secret (`authorizationToken`, `sessionCookies`,
432
+ * `expiresAt`) and its binding (`issuedFor`, `issuedBy`) laid over them; `null` when neither store holds anything.
433
+ * Means a session store answers are not read, nor a secret a key store
434
+ * answers.
272
435
  */
273
436
  async getConnectionConfig(destination) {
274
- const sessionConn = await this.read(destination, 'session connection config', () => this.sessionStore.getConnectionConfig(destination));
275
- if (sessionConn) {
276
- return sessionConn;
437
+ const serviceKeyStore = this.serviceKeyStore;
438
+ const means = serviceKeyStore
439
+ ? await this.read(destination, 'service key connection config', () => serviceKeyStore.getConnectionConfig(destination))
440
+ : null;
441
+ const secret = await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
442
+ const composed = {};
443
+ for (const [field, value] of Object.entries(means ?? {})) {
444
+ if (!isSecretField(field) && value !== undefined) {
445
+ composed[field] = value;
446
+ }
447
+ }
448
+ for (const field of SECRET_FIELDS) {
449
+ const value = secret?.[field];
450
+ if (value !== undefined) {
451
+ composed[field] = value;
452
+ }
277
453
  }
454
+ return Object.keys(composed).length > 0 ? composed : null;
455
+ }
456
+ /**
457
+ * The credential for the destination, ready for a connector: the provider
458
+ * its configuration states (`authType`, and `grantType` for `jwt` / `saml`),
459
+ * built from the means the service key store holds and the secret the
460
+ * session store holds.
461
+ *
462
+ * One provider per destination for the broker's life: concurrent first calls
463
+ * share one build, and a build that threw is tried again on the next call.
464
+ *
465
+ * A token provider (the UAA, OIDC and SAML grants) is seeded from the
466
+ * session when the stored secret is bound to this destination, and writes
467
+ * every token it obtains — at `prepare()`, on expiry, or in `rejected()` after
468
+ * a 401 — back to the session store before it answers (see `flush()`).
469
+ *
470
+ * @throws DestinationConfigError when the destination lacks what its type
471
+ * needs — naming the fields or options, never a value.
472
+ */
473
+ getProvider(destination) {
474
+ const cached = this.built.get(destination);
475
+ if (cached) {
476
+ return cached;
477
+ }
478
+ // Deferred by one tick, so the promise is in the cache before the build
479
+ // reads anything.
480
+ const build = Promise.resolve().then(() => this.build(destination));
481
+ this.built.set(destination, build);
482
+ build.catch(() => {
483
+ if (this.built.get(destination) === build) {
484
+ this.built.delete(destination);
485
+ }
486
+ });
487
+ return build;
488
+ }
489
+ async build(destination) {
278
490
  const serviceKeyStore = this.serviceKeyStore;
279
491
  if (!serviceKeyStore) {
280
- return null;
492
+ throw new DestinationConfigError_1.DestinationConfigError(destination, ['serviceKeyStore'], 'getProvider reads the means from a service key store, and none was given');
493
+ }
494
+ const means = await this.read(destination, 'means', () => serviceKeyStore.getConnectionConfig(destination));
495
+ const authType = (0, destinations_1.statedAuthType)(destination, means);
496
+ // statedAuthType refuses a destination without means.
497
+ const stated = means;
498
+ let provider;
499
+ if (authType === 'basic') {
500
+ provider = (0, destinations_1.basicProvider)(destination, stated);
501
+ }
502
+ else if (authType === 'snc') {
503
+ provider = (0, destinations_1.sncProvider)(destination, stated, this.logger);
281
504
  }
282
- return this.read(destination, 'service key connection config', () => serviceKeyStore.getConnectionConfig(destination));
505
+ else {
506
+ const grant = (0, destinations_1.statedGrant)(destination, authType, stated);
507
+ if (grant !== 'none') {
508
+ // A grant that obtains a secret: the row's client (none for
509
+ // saml2_pure), what a secret for this destination is bound to
510
+ // — written with every secret, and required of a stored one
511
+ // before it seeds — and the stored secret when it is.
512
+ const client = grant === 'saml2_pure'
513
+ ? null
514
+ : await this.read(destination, 'client', () => serviceKeyStore.getAuthorizationConfig(destination));
515
+ const binding = (0, bindingOf_1.destinationBinding)(authType, grant, stated, client);
516
+ const stored = grant === 'client_credentials'
517
+ ? null
518
+ : await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
519
+ const secret = this.boundOrDiscarded(destination, stored, binding);
520
+ // The expiry is fixed when the result arrives, not when a retry
521
+ // finally writes it — on a copy; the outcome of the write is recorded
522
+ // against the result itself, which the provider also returns to the
523
+ // token API.
524
+ const onTokens = (result) => this.writer.submit(destination, {
525
+ result: { ...result, expiresAt: expiryOf(result) },
526
+ original: result,
527
+ binding,
528
+ carry: 'bound',
529
+ });
530
+ const common = {
531
+ destination,
532
+ client,
533
+ secret,
534
+ logger: this.logger,
535
+ onTokens,
536
+ };
537
+ if ((0, destinations_1.isUaaGrant)(grant)) {
538
+ provider = (0, destinations_1.uaaProvider)({
539
+ ...common,
540
+ grant,
541
+ authorization: this.authorization,
542
+ });
543
+ }
544
+ else if ((0, destinations_1.isOidcGrant)(grant)) {
545
+ provider = (0, destinations_1.oidcProvider)({
546
+ ...common,
547
+ grant,
548
+ means: stated,
549
+ oidcAuthorization: this.oidcAuthorization,
550
+ deviceCodePresenter: this.deviceCodePresenter,
551
+ });
552
+ }
553
+ else if ((0, destinations_1.isSamlGrant)(grant)) {
554
+ provider = (0, destinations_1.samlProvider)({
555
+ ...common,
556
+ grant,
557
+ means: stated,
558
+ authorization: this.authorization,
559
+ samlCookies: this.samlCookies,
560
+ assertionReplayStore: this.assertionReplayStore,
561
+ });
562
+ }
563
+ else {
564
+ // statedGrant admits only the allowed `authType` / `grantType` pairs.
565
+ throw new Error(`unreachable grant ${grant}`);
566
+ }
567
+ this.logger.debug(`[AuthBroker] Provider built for ${destination}`, {
568
+ authType,
569
+ grant,
570
+ seeded: !!secret,
571
+ });
572
+ return provider;
573
+ }
574
+ const secret = await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
575
+ const client = authType === 'jwt'
576
+ ? await this.read(destination, 'client', () => serviceKeyStore.getAuthorizationConfig(destination))
577
+ : null;
578
+ provider = (0, destinations_1.handedOverProvider)(destination, authType, secret, (0, bindingOf_1.destinationBinding)(authType, 'none', stated, client));
579
+ }
580
+ this.logger.debug(`[AuthBroker] Provider built for ${destination}`, {
581
+ authType,
582
+ });
583
+ return provider;
584
+ }
585
+ /**
586
+ * The stored secret when it is bound to this destination's resource and
587
+ * issuer, else `null`: the provider is then built as with no session
588
+ * and logs in afresh — the refresh token is not spent either. The log line
589
+ * names the destination only: never a URI, never a token.
590
+ */
591
+ boundOrDiscarded(destination, stored, binding) {
592
+ if (!stored || (0, binding_1.boundHere)(stored, binding))
593
+ return stored;
594
+ if (present(stored.authorizationToken) ||
595
+ present(stored.sessionCookies) ||
596
+ present(stored.refreshToken)) {
597
+ this.logger.warn(`[AuthBroker] ${destination}: secret bound to another resource, discarded`);
598
+ }
599
+ return null;
600
+ }
601
+ /**
602
+ * Waits for the session writes still pending — each gets one more attempt —
603
+ * and rejects naming the destinations whose store still refuses. Call it on
604
+ * shutdown to know whether every token a provider obtained is stored; the
605
+ * broker keeps retrying after a rejection.
606
+ */
607
+ flush() {
608
+ return this.writer.flush();
609
+ }
610
+ /**
611
+ * One write of the destination's session secret — and nothing else:
612
+ * `{ authorizationToken, expiresAt, refreshToken, issuedFor, issuedBy }` in
613
+ * one `saveSession` — or, for a `saml2_pure` result (`tokenType: 'saml'`),
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.
618
+ *
619
+ * - `expiresAt`: fixed by `onTokens` when the result arrived — the result's
620
+ * own, else the `expiresIn` it reports counted from then (the rule the
621
+ * provider applies to its own cache).
622
+ * - `refreshToken`: the result's, else the one the session holds, read at
623
+ * write time, so a result without one does not erase the stored one — for
624
+ * a provider the broker built, only a stored one bound where this one is:
625
+ * a refresh token obtained for another resource or from another client is
626
+ * not carried into this secret. A consumer's provider was seeded
627
+ * with whatever the session held, so whatever it holds is carried.
628
+ * - `issuedFor` / `issuedBy`: the binding computed when the provider was
629
+ * built, each left out when the means lack its source — so the
630
+ * store clears it.
631
+ * - A destination the key store now states as `basic` or `snc` is not
632
+ * written: those obtain no session secret.
633
+ */
634
+ async writeSecret(destination, { result, binding, carry }) {
635
+ const serviceKeyStore = this.serviceKeyStore;
636
+ const means = serviceKeyStore
637
+ ? await this.read(destination, 'means', () => serviceKeyStore.getConnectionConfig(destination))
638
+ : null;
639
+ if (means?.authType === 'basic' || means?.authType === 'snc') {
640
+ this.logger.warn(`[AuthBroker] Not written: ${destination} is a ${means.authType} destination, which holds no session secret`);
641
+ return;
642
+ }
643
+ let secret;
644
+ if (result.tokenType === 'saml') {
645
+ // saml2_pure: the cookies are the credential, and SAML has no refresh
646
+ // token to carry.
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
+ };
665
+ }
666
+ if (binding.issuedFor !== undefined)
667
+ secret.issuedFor = binding.issuedFor;
668
+ if (binding.issuedBy !== undefined)
669
+ secret.issuedBy = binding.issuedBy;
670
+ await this.sessionStore.saveSession(destination, secret);
671
+ this.logger.info(`[AuthBroker] Session secret saved for ${destination}`, {
672
+ credential: secret.sessionCookies !== undefined ? 'cookies' : 'token',
673
+ hasRefreshToken: !!secret.refreshToken,
674
+ expiresAt: result.expiresAt,
675
+ });
283
676
  }
284
677
  /**
285
678
  * An `ITokenRefresher` for one destination, for injection into a connection: