@mcp-abap-adt/auth-broker 4.1.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.
@@ -5,21 +5,38 @@
5
5
  * Two fields choose the provider — `authType` and, for `jwt` and `saml`,
6
6
  * `grantType` — and nothing else: no field's presence or absence is ever
7
7
  * read to pick a row. Both come from the key store's means.
8
+ *
9
+ * Every token row takes the consumer's `renewal` (called once per build, with
10
+ * the destination and its grant) and persists through auth-providers'
11
+ * `refreshStatePersistence` over the broker's write, governed by the
12
+ * consumer's `onWriteFailure`; a row built without either is refused naming
13
+ * it, beside every other missing field, in one error. A failure the broker
14
+ * reads — the SNC provider's, a validator's, a provider constructor's — is
15
+ * read through auth-errors (`readFailure`) and carried by the
16
+ * `DestinationConfigError`, never matched by class.
8
17
  */
9
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.isStatedRow = isStatedRow;
10
20
  exports.statedAuthType = statedAuthType;
11
21
  exports.statedGrant = statedGrant;
12
22
  exports.basicProvider = basicProvider;
13
23
  exports.sncProvider = sncProvider;
14
24
  exports.handedOverProvider = handedOverProvider;
25
+ exports.isWriteFailurePolicy = isWriteFailurePolicy;
26
+ exports.optionsLacking = optionsLacking;
15
27
  exports.isUaaGrant = isUaaGrant;
28
+ exports.uaaRefusal = uaaRefusal;
16
29
  exports.uaaProvider = uaaProvider;
17
30
  exports.isOidcGrant = isOidcGrant;
31
+ exports.oidcRefusal = oidcRefusal;
18
32
  exports.oidcProvider = oidcProvider;
19
33
  exports.isSamlGrant = isSamlGrant;
34
+ exports.samlRefusal = samlRefusal;
20
35
  exports.samlProvider = samlProvider;
36
+ const auth_errors_1 = require("@mcp-abap-adt/auth-errors");
21
37
  const auth_providers_1 = require("@mcp-abap-adt/auth-providers");
22
38
  const binding_1 = require("./binding");
39
+ const contractShape_1 = require("./contractShape");
23
40
  const DestinationConfigError_1 = require("./DestinationConfigError");
24
41
  /** The allowed `authType` / `grantType` pairs. `basic` and `snc` read no grant. */
25
42
  const GRANTS = {
@@ -36,10 +53,39 @@ const GRANTS = {
36
53
  saml: ['saml2_pure', 'saml2_bearer', 'none'],
37
54
  };
38
55
  const AUTH_TYPES = ['basic', 'jwt', 'saml', 'snc'];
56
+ /**
57
+ * `authType` and `grantType` are a `jwt` / `saml` pair of the closed list —
58
+ * so neither holds `;` or `/`. Never throws.
59
+ */
60
+ function isStatedRow(authType, grant) {
61
+ if (authType !== 'jwt' && authType !== 'saml')
62
+ return false;
63
+ return GRANTS[authType].includes(grant);
64
+ }
39
65
  /** A stored string that counts as present: `''` is missing. */
40
66
  function present(value) {
41
67
  return typeof value === 'string' && value !== '';
42
68
  }
69
+ /** Not stated: absent or `null`. */
70
+ function unstated(value) {
71
+ return value === undefined || value === null;
72
+ }
73
+ /**
74
+ * Whether a trust input the binding hashes has its expected shape —
75
+ * or is not stated. One of another shape is refused naming it, never hashed
76
+ * as if it were absent: two different malformed values would otherwise give
77
+ * one digest.
78
+ */
79
+ function trustShaped(value, shape) {
80
+ if (unstated(value))
81
+ return true;
82
+ if (shape === 'string')
83
+ return typeof value === 'string';
84
+ if (shape === 'boolean')
85
+ return typeof value === 'boolean';
86
+ return (Array.isArray(value) &&
87
+ value.every((element) => typeof element === 'string'));
88
+ }
43
89
  /** The stated `authType`, or the error naming it. */
44
90
  function statedAuthType(destination, means) {
45
91
  const authType = means?.authType;
@@ -79,8 +125,8 @@ const CERTIFICATE_HINT = 'a certificate client needs a clientAuthentication stra
79
125
  * gap, and gets 4.0.0's words alone. Decided from what was read, never by
80
126
  * asking the store about a certificate.
81
127
  */
82
- function noClientHint(clientAuthentication, client) {
83
- return !clientAuthentication && !present(client?.uaaClientId);
128
+ function noClientHint(clientAuthenticated, client) {
129
+ return !clientAuthenticated && !present(client?.uaaClientId);
84
130
  }
85
131
  /** A client row's refusal: 4.0.0's words, and the hint when `hinted`. */
86
132
  function clientRowError(destination, lacking, reason, hinted) {
@@ -120,30 +166,55 @@ function sncProvider(destination, means, logger) {
120
166
  throw new DestinationConfigError_1.DestinationConfigError(destination, ['sncPartnerName'], 'an snc destination needs the system’s SNC name');
121
167
  }
122
168
  try {
123
- return auth_providers_1.SncLogonProvider.forSecureLoginClient({
169
+ return auth_providers_1.SncLogonProvider.forSecureLoginClient((0, contractShape_1.asContract)({
124
170
  partnerName: means.sncPartnerName,
125
171
  qop: present(means.sncQop) ? means.sncQop : undefined,
126
172
  sncLib: present(means.sncLib) ? means.sncLib : undefined,
127
173
  myName: present(means.sncMyName) ? means.sncMyName : undefined,
128
174
  logger,
129
- });
175
+ }));
130
176
  }
131
- catch (error) {
132
- if (error instanceof auth_providers_1.ValidationError) {
133
- throw new DestinationConfigError_1.DestinationConfigError(destination, (error.missingFields ?? []).map((field) => SNC_FIELDS[field] ?? field), 'the SNC provider refused the destination’s SNC settings');
134
- }
135
- throw error;
177
+ catch (thrown) {
178
+ // Decided by kind, read through auth-errors — never by class: a failure
179
+ // of another installed copy of auth-errors reads the same.
180
+ const error = (0, auth_errors_1.readFailure)(thrown, 'resolving-snc-library');
181
+ const fields = error.kind === 'configuration'
182
+ ? mappedFields(error.facts.fields, SNC_FIELDS)
183
+ : SNC_STORE_FIELDS.filter((field) => present(means[field]));
184
+ throw new DestinationConfigError_1.DestinationConfigError(destination, fields, 'the SNC provider refused the destination’s SNC settings', error);
136
185
  }
137
186
  }
187
+ /** The four SNC fields of the means, in the order a refusal names them. */
188
+ const SNC_STORE_FIELDS = [
189
+ 'sncPartnerName',
190
+ 'sncQop',
191
+ 'sncLib',
192
+ 'sncMyName',
193
+ ];
194
+ /**
195
+ * A provider's configuration field names as the store's (or the broker's
196
+ * option names), through a row's map; a name the map does not hold is kept —
197
+ * it is one of interfaces-auth's `CONFIG_FIELDS`, a name, never a value.
198
+ */
199
+ function mappedFields(fields, map) {
200
+ const names = [];
201
+ for (const field of fields) {
202
+ const name = map[field] ?? field;
203
+ if (!names.includes(name))
204
+ names.push(name);
205
+ }
206
+ return names;
207
+ }
138
208
  /**
139
209
  * `none` → the credential handed over, from the session: the only way in, so
140
210
  * required. Read from the secret alone — the means are not a seed.
141
211
  *
142
212
  * The broker did not obtain it and cannot obtain it again, so a binding that
143
- * does not match is refused, never discarded: `issuedFor`
144
- * must always equal the destination's resource — it is what stops the
145
- * credential going to another one; `issuedBy` is compared only when the means
146
- * state an issuer.
213
+ * does not match is refused, never discarded: `issuedFor` must equal the
214
+ * destination's resource — it is what stops the credential going to another
215
+ * one — and `issuedBy` must be exactly the row's record: a credential
216
+ * handed over with a 4.x binding, or none, is refused until it is written
217
+ * again with 5.0.0's `bindingOf`.
147
218
  */
148
219
  function handedOverProvider(destination, authType, secret, binding) {
149
220
  const credential = authType === 'jwt' ? secret?.authorizationToken : secret?.sessionCookies;
@@ -155,9 +226,8 @@ function handedOverProvider(destination, authType, secret, binding) {
155
226
  const unbound = [];
156
227
  if (!(0, binding_1.sameResource)(secret, binding))
157
228
  unbound.push('issuedFor');
158
- if (binding.issuerStated && !(0, binding_1.sameIssuer)(secret, binding)) {
229
+ if (!(0, binding_1.sameRecord)(secret, binding))
159
230
  unbound.push('issuedBy');
160
- }
161
231
  if (unbound.length > 0) {
162
232
  throw new DestinationConfigError_1.DestinationConfigError(destination, unbound, 'the credential in the session is not bound to this destination’s means');
163
233
  }
@@ -165,87 +235,218 @@ function handedOverProvider(destination, authType, secret, binding) {
165
235
  ? auth_providers_1.TokenAuthProvider.fixed(credential)
166
236
  : new auth_providers_1.SamlAuthProvider(credential);
167
237
  }
238
+ /** `onWriteFailure` as given: `'fail'` or `'continue'`, nothing else. */
239
+ function isWriteFailurePolicy(value) {
240
+ return value === 'fail' || value === 'continue';
241
+ }
242
+ /**
243
+ * The consumer options a token row lacks, by name: `renewal` when it is no
244
+ * function, `onWriteFailure` when it is neither `'fail'` nor `'continue'`.
245
+ * There is no default for either.
246
+ */
247
+ function optionsLacking(options) {
248
+ const lacking = [];
249
+ if (typeof options.renewal !== 'function')
250
+ lacking.push('renewal');
251
+ if (!isWriteFailurePolicy(options.onWriteFailure)) {
252
+ lacking.push('onWriteFailure');
253
+ }
254
+ return lacking;
255
+ }
256
+ /**
257
+ * The row's renewal strategy: `renewal(destination, grant)`, called once per
258
+ * build after every other check has passed. A throw is refused naming
259
+ * `renewal`, carrying what it threw as auth-errors reads it.
260
+ */
261
+ function renewalFor(destination, grant, renewal) {
262
+ try {
263
+ // Checked by optionsLacking before any row gets here.
264
+ return renewal(destination, grant);
265
+ }
266
+ catch (thrown) {
267
+ throw new DestinationConfigError_1.DestinationConfigError(destination, ['renewal'], 'the renewal option failed', (0, auth_errors_1.readFailure)(thrown, 'renewal-strategy'));
268
+ }
269
+ }
270
+ /**
271
+ * The row's persistence: auth-providers' `refreshStatePersistence` over the
272
+ * broker's write, with the consumer's `onWriteFailure` as given.
273
+ */
274
+ function persistenceFor(options) {
275
+ return (0, auth_providers_1.refreshStatePersistence)(options.write, (0, contractShape_1.asContract)({
276
+ // Checked by optionsLacking before any row gets here.
277
+ onWriteFailure: options.onWriteFailure,
278
+ // No logger: the broker's writer logs every failed write once, naming
279
+ // the destination; the persistence's own line would repeat it.
280
+ }));
281
+ }
282
+ /**
283
+ * Builds the provider — the persistence strategy and the constructor —
284
+ * inside one guard: a refusal of the configuration the row gave it is a
285
+ * `DestinationConfigError` naming the store fields (or broker options) its
286
+ * facts name, through the row's field map, carrying the provider's error.
287
+ */
288
+ function constructed(destination, fieldMap, build) {
289
+ try {
290
+ return build();
291
+ }
292
+ catch (thrown) {
293
+ const error = (0, auth_errors_1.readFailure)(thrown, 'unfamiliar-error');
294
+ throw new DestinationConfigError_1.DestinationConfigError(destination, error.kind === 'configuration'
295
+ ? mappedFields(error.facts.fields, fieldMap)
296
+ : [], 'the provider refused the configuration the destination states', error);
297
+ }
298
+ }
299
+ /** The fields every token row's provider shares, as the store and the broker name them. */
300
+ const COMMON_FIELDS = {
301
+ clientId: 'uaaClientId',
302
+ clientSecret: 'uaaClientSecret',
303
+ uaaUrl: 'uaaUrl',
304
+ renewal: 'renewal',
305
+ persistence: 'onWriteFailure',
306
+ onWriteFailure: 'onWriteFailure',
307
+ authorization: 'authorization',
308
+ clientAuthentication: 'clientAuthentication',
309
+ };
310
+ const OIDC_FIELDS = {
311
+ ...COMMON_FIELDS,
312
+ issuerUrl: 'oidcIssuerUrl',
313
+ tokenEndpoint: 'oidcTokenEndpoint',
314
+ authorizationEndpoint: 'oidcAuthorizationEndpoint',
315
+ deviceAuthorizationEndpoint: 'oidcDeviceAuthorizationEndpoint',
316
+ scopes: 'oidcScopes',
317
+ scope: 'oidcScopes',
318
+ audience: 'oidcAudience',
319
+ subjectToken: 'oidcSubjectToken',
320
+ subjectTokenType: 'oidcSubjectTokenType',
321
+ actorToken: 'oidcActorToken',
322
+ actorTokenType: 'oidcActorTokenType',
323
+ authorization: 'oidcAuthorization',
324
+ presenter: 'deviceCodePresenter',
325
+ };
326
+ const SAML_FIELDS = {
327
+ ...COMMON_FIELDS,
328
+ idpSsoUrl: 'samlIdpSsoUrl',
329
+ spEntityId: 'samlSpEntityId',
330
+ acsUrl: 'samlAcsUrl',
331
+ relayState: 'samlRelayState',
332
+ idpEntityId: 'samlIdpEntityId',
333
+ idpInitiated: 'samlIdpInitiated',
334
+ idpCertificates: 'samlIdpCertificates',
335
+ assertionValidator: 'samlIdpCertificates',
336
+ clockSkewMs: 'samlClockSkewMs',
337
+ tokenUrl: 'samlTokenUrl',
338
+ cookieProvider: 'samlCookies',
339
+ };
168
340
  function isUaaGrant(grant) {
169
341
  return (grant === 'authorization_code' ||
170
342
  grant === 'client_credentials' ||
171
343
  grant === 'passcode');
172
344
  }
173
345
  /**
174
- * `jwt` / `authorization_code` → `AuthorizationCodeProvider`,
175
- * `client_credentials` → `ClientCredentialsProvider`, `passcode` →
176
- * `UaaPasscodeProvider`: the client from the key store, the seed from the
177
- * session (not for `client_credentials`, whose row takes the client alone).
178
- *
179
- * Every field and option the row lacks is named in one error, before the
180
- * consumer's `authorization` is called. `serviceUrl` is not among them: the
181
- * providers read the client and `uaaUrl` only. Without a strategy,
182
- * `uaaClientSecret: ''` is a public client: `passcode` takes it as no secret;
183
- * the other two rows' providers require a secret, so for them `''` is
184
- * missing. On the strategy path the client may be a certificate client's
185
- * identity, which holds no secret field at all (`RowClient`).
186
- *
187
- * With the strategy's answer (`clientAuthentication`) no secret is required
188
- * and none is passed: the provider authenticates its client with the answer.
346
+ * Every field and option a UAA row lacks, named in one error — or nothing.
347
+ * `serviceUrl` is not among them: the providers read the client and `uaaUrl`
348
+ * only. Without a strategy, `uaaClientSecret: ''` is a public client:
349
+ * `passcode` takes it as no secret; the other two rows' providers require a
350
+ * secret, so for them `''` is missing.
189
351
  */
190
- function uaaProvider(row) {
191
- const { destination, grant, client, secret, clientAuthentication } = row;
352
+ function uaaRefusal(check) {
353
+ const { destination, grant, client, clientAuthenticated } = check;
192
354
  const lacking = [];
193
- if (!present(client?.uaaUrl))
194
- lacking.push('uaaUrl');
195
- if (!present(client?.uaaClientId))
196
- lacking.push('uaaClientId');
197
- if (!clientAuthentication &&
355
+ if (client !== undefined) {
356
+ if (!present(client?.uaaUrl))
357
+ lacking.push('uaaUrl');
358
+ if (!present(client?.uaaClientId))
359
+ lacking.push('uaaClientId');
360
+ }
361
+ if (!clientAuthenticated &&
198
362
  grant !== 'passcode' &&
199
- !present(secretOf(client))) {
363
+ !present(secretOf(client ?? null))) {
200
364
  lacking.push('uaaClientSecret');
201
365
  }
202
- if (grant !== 'client_credentials' && !row.authorization) {
366
+ if (grant !== 'client_credentials' && !check.authorization) {
203
367
  lacking.push('authorization');
204
368
  }
205
- if (lacking.length > 0) {
206
- throw clientRowError(destination, lacking, `a jwt destination with grantType ${grant} lacks what its grant needs`, noClientHint(clientAuthentication, client));
207
- }
369
+ lacking.push(...optionsLacking(check));
370
+ if (lacking.length === 0)
371
+ return undefined;
372
+ return clientRowError(destination, lacking, `a jwt destination with grantType ${grant} lacks what its grant needs`, noClientHint(clientAuthenticated, client));
373
+ }
374
+ /**
375
+ * `jwt` / `authorization_code` → `AuthorizationCodeProvider`,
376
+ * `client_credentials` → `ClientCredentialsProvider`, `passcode` →
377
+ * `UaaPasscodeProvider`: the client from the key store, the seed from the
378
+ * session (not for `client_credentials`, whose row takes the client alone),
379
+ * the consumer's renewal for this grant, and the broker's persistence.
380
+ *
381
+ * Every field and option the row lacks is named in one error
382
+ * (`uaaRefusal`), before the consumer's `authorization` or `renewal` is
383
+ * called. On the strategy path the client may be a certificate client's
384
+ * identity, which holds no secret field at all (`RowClient`). With the
385
+ * strategy's answer (`clientAuthentication`) no secret is required and none
386
+ * is passed: the provider authenticates its client with the answer.
387
+ */
388
+ function uaaProvider(row) {
389
+ const { destination, grant, client, secret, clientAuthentication } = row;
390
+ const refusal = uaaRefusal({
391
+ ...row,
392
+ clientAuthenticated: !!clientAuthentication,
393
+ });
394
+ if (refusal)
395
+ throw refusal;
208
396
  const uaaUrl = client?.uaaUrl;
209
397
  const clientId = client?.uaaClientId;
210
398
  const clientSecret = !clientAuthentication && present(secretOf(client))
211
399
  ? secretOf(client)
212
400
  : undefined;
213
- const hooks = {
214
- onTokens: row.onTokens,
401
+ const renewal = renewalFor(destination, grant, row.renewal);
402
+ const hooks = () => ({
403
+ renewal,
404
+ persistence: persistenceFor(row),
215
405
  ...authenticatedBy(clientAuthentication),
216
- };
406
+ authDebug: row.authDebug,
407
+ });
217
408
  if (grant === 'client_credentials') {
218
- return new auth_providers_1.ClientCredentialsProvider({
219
- uaaUrl,
220
- clientId,
221
- ...(clientSecret === undefined ? {} : { clientSecret }),
222
- logger: row.logger,
223
- ...hooks,
224
- });
409
+ // The client alone: no seed.
410
+ return {
411
+ provider: constructed(destination, COMMON_FIELDS, () => new auth_providers_1.ClientCredentialsProvider({
412
+ uaaUrl,
413
+ clientId,
414
+ ...(clientSecret === undefined ? {} : { clientSecret }),
415
+ logger: row.logger,
416
+ ...hooks(),
417
+ })),
418
+ seededRefreshToken: undefined,
419
+ };
225
420
  }
226
421
  const seed = tokenSeed(secret);
227
422
  // Checked above: the rows that reach here need it.
228
423
  const authorization = row.authorization(destination, grant);
229
424
  if (grant === 'authorization_code') {
230
- return new auth_providers_1.AuthorizationCodeProvider({
425
+ return {
426
+ provider: constructed(destination, COMMON_FIELDS, () => new auth_providers_1.AuthorizationCodeProvider((0, contractShape_1.asContract)({
427
+ uaaUrl,
428
+ clientId,
429
+ ...(clientSecret === undefined ? {} : { clientSecret }),
430
+ authorization,
431
+ ...seed,
432
+ logger: row.logger,
433
+ ...hooks(),
434
+ }))),
435
+ seededRefreshToken: seed.refreshToken,
436
+ };
437
+ }
438
+ return {
439
+ provider: constructed(destination, COMMON_FIELDS, () => new auth_providers_1.UaaPasscodeProvider((0, contractShape_1.asContract)({
231
440
  uaaUrl,
232
441
  clientId,
233
- ...(clientSecret === undefined ? {} : { clientSecret }),
442
+ clientSecret,
234
443
  authorization,
235
444
  ...seed,
236
445
  logger: row.logger,
237
- ...hooks,
238
- });
239
- }
240
- return new auth_providers_1.UaaPasscodeProvider({
241
- uaaUrl,
242
- clientId,
243
- clientSecret,
244
- authorization,
245
- ...seed,
246
- logger: row.logger,
247
- ...hooks,
248
- });
446
+ ...hooks(),
447
+ }))),
448
+ seededRefreshToken: seed.refreshToken,
449
+ };
249
450
  }
250
451
  /** The seed of a token provider: the stored token, its refresh token and expiry. */
251
452
  function tokenSeed(secret) {
@@ -280,47 +481,74 @@ const OIDC_ENDPOINTS = {
280
481
  token_exchange: ['oidcTokenEndpoint'],
281
482
  };
282
483
  /**
283
- * `jwt` / `oidc_authorization_code` → `OidcBrowserProvider`, `device_code` →
284
- * `OidcDeviceFlowProvider`, `password` → `OidcPasswordProvider`,
285
- * `token_exchange` → `OidcTokenExchangeProvider`: the client id (and secret,
286
- * `''` being a public client) from the key store's client, the grant's data
287
- * from its `oidc*` fields, the seed from the session.
288
- *
484
+ * Every field and option an OIDC row lacks, named in one error — or nothing.
289
485
  * The endpoints: `oidcIssuerUrl`, from which the provider discovers them, or
290
486
  * every explicit endpoint the row uses — without either the error names the
291
- * issuer and the endpoints missing. Every field and option the row lacks is
292
- * named in one error, before any collaborator is called. `token_exchange`
293
- * takes one scope string: the stored scopes joined by a space.
487
+ * issuer and the endpoints missing.
294
488
  */
295
- function oidcProvider(row) {
296
- const { destination, grant, means, client } = row;
489
+ function oidcRefusal(check) {
490
+ const { destination, grant, means, client } = check;
297
491
  const lacking = [];
298
- if (!present(client?.uaaClientId))
492
+ if (client !== undefined && !present(client?.uaaClientId)) {
299
493
  lacking.push('uaaClientId');
494
+ }
300
495
  if (!present(means.oidcIssuerUrl)) {
301
496
  const endpoints = OIDC_ENDPOINTS[grant].filter((name) => !present(means[name]));
302
497
  if (endpoints.length > 0)
303
498
  lacking.push('oidcIssuerUrl', ...endpoints);
304
499
  }
500
+ if (!trustShaped(means.oidcScopes, 'strings'))
501
+ lacking.push('oidcScopes');
305
502
  if (grant === 'password') {
306
503
  lacking.push(...missing(means, ['username', 'password']));
307
504
  }
308
505
  if (grant === 'token_exchange') {
309
506
  lacking.push(...missing(means, ['oidcSubjectToken', 'oidcSubjectTokenType']));
507
+ if (!trustShaped(means.oidcActorTokenType, 'string')) {
508
+ lacking.push('oidcActorTokenType');
509
+ }
310
510
  }
311
- if (grant === 'oidc_authorization_code' && !row.oidcAuthorization) {
511
+ if (grant === 'oidc_authorization_code' && !check.oidcAuthorization) {
312
512
  lacking.push('oidcAuthorization');
313
513
  }
314
- if (grant === 'device_code' && !row.deviceCodePresenter) {
514
+ if (grant === 'device_code' && !check.deviceCodePresenter) {
315
515
  lacking.push('deviceCodePresenter');
316
516
  }
317
- if (lacking.length > 0) {
318
- throw clientRowError(destination, lacking, `a jwt destination with grantType ${grant} lacks what its grant needs`, noClientHint(row.clientAuthentication, client));
319
- }
517
+ lacking.push(...optionsLacking(check));
518
+ if (lacking.length === 0)
519
+ return undefined;
520
+ return clientRowError(destination, lacking, `a jwt destination with grantType ${grant} lacks what its grant needs`, noClientHint(check.clientAuthenticated, client));
521
+ }
522
+ /**
523
+ * `jwt` / `oidc_authorization_code` → `OidcBrowserProvider`, `device_code` →
524
+ * `OidcDeviceFlowProvider`, `password` → `OidcPasswordProvider`,
525
+ * `token_exchange` → `OidcTokenExchangeProvider`: the client id (and secret,
526
+ * `''` being a public client) from the key store's client, the grant's data
527
+ * from its `oidc*` fields, the seed from the session, the consumer's renewal
528
+ * for this grant, and the broker's persistence.
529
+ *
530
+ * Every field and option the row lacks is named in one error
531
+ * (`oidcRefusal`), before any collaborator is called. `token_exchange` takes
532
+ * one scope string: the stored scopes joined by a space.
533
+ */
534
+ function oidcProvider(row) {
535
+ const { destination, grant, means, client } = row;
536
+ const refusal = oidcRefusal({
537
+ ...row,
538
+ clientAuthenticated: !!row.clientAuthentication,
539
+ });
540
+ if (refusal)
541
+ throw refusal;
320
542
  const scopes = Array.isArray(means.oidcScopes) && means.oidcScopes.length > 0
321
543
  ? means.oidcScopes
322
544
  : undefined;
323
- const common = {
545
+ const renewal = renewalFor(destination, grant, row.renewal);
546
+ const seed = tokenSeed(row.secret);
547
+ const built = (provider) => ({
548
+ provider,
549
+ seededRefreshToken: seed.refreshToken,
550
+ });
551
+ const common = () => ({
324
552
  issuerUrl: stated(means.oidcIssuerUrl),
325
553
  clientId: client?.uaaClientId,
326
554
  clientSecret: row.clientAuthentication
@@ -328,48 +556,86 @@ function oidcProvider(row) {
328
556
  : stated(secretOf(client)),
329
557
  ...authenticatedBy(row.clientAuthentication),
330
558
  tokenEndpoint: stated(means.oidcTokenEndpoint),
331
- ...tokenSeed(row.secret),
559
+ ...seed,
332
560
  logger: row.logger,
333
- onTokens: row.onTokens,
334
- };
561
+ authDebug: row.authDebug,
562
+ renewal,
563
+ persistence: persistenceFor(row),
564
+ });
335
565
  switch (grant) {
336
- case 'oidc_authorization_code':
337
- return new auth_providers_1.OidcBrowserProvider({
338
- ...common,
566
+ case 'oidc_authorization_code': {
567
+ // Checked above.
568
+ const authorization = row.oidcAuthorization(destination);
569
+ return built(constructed(destination, OIDC_FIELDS, () => new auth_providers_1.OidcBrowserProvider((0, contractShape_1.asContract)({
570
+ ...common(),
339
571
  scopes,
340
572
  authorizationEndpoint: stated(means.oidcAuthorizationEndpoint),
341
- // Checked above.
342
- authorization: row.oidcAuthorization(destination),
343
- });
344
- case 'device_code':
345
- return new auth_providers_1.OidcDeviceFlowProvider({
346
- ...common,
573
+ authorization,
574
+ }))));
575
+ }
576
+ case 'device_code': {
577
+ const presenter = row.deviceCodePresenter(destination);
578
+ return built(constructed(destination, OIDC_FIELDS, () => new auth_providers_1.OidcDeviceFlowProvider((0, contractShape_1.asContract)({
579
+ ...common(),
347
580
  scopes,
348
581
  deviceAuthorizationEndpoint: stated(means.oidcDeviceAuthorizationEndpoint),
349
- presenter: row.deviceCodePresenter(destination),
350
- });
582
+ presenter,
583
+ }))));
584
+ }
351
585
  case 'password':
352
- return new auth_providers_1.OidcPasswordProvider({
353
- ...common,
586
+ return built(constructed(destination, OIDC_FIELDS, () => new auth_providers_1.OidcPasswordProvider((0, contractShape_1.asContract)({
587
+ ...common(),
354
588
  scopes,
355
589
  username: means.username,
356
590
  password: means.password,
357
- });
591
+ }))));
358
592
  case 'token_exchange':
359
- return new auth_providers_1.OidcTokenExchangeProvider({
360
- ...common,
593
+ return built(constructed(destination, OIDC_FIELDS, () => new auth_providers_1.OidcTokenExchangeProvider((0, contractShape_1.asContract)({
594
+ ...common(),
361
595
  scope: scopes?.join(' '),
362
596
  subjectToken: means.oidcSubjectToken,
363
597
  subjectTokenType: means.oidcSubjectTokenType,
364
598
  audience: stated(means.oidcAudience),
365
599
  actorToken: stated(means.oidcActorToken),
366
600
  actorTokenType: stated(means.oidcActorTokenType),
367
- });
601
+ }))));
368
602
  }
369
603
  }
370
604
  function isSamlGrant(grant) {
371
605
  return grant === 'saml2_pure' || grant === 'saml2_bearer';
372
606
  }
607
+ /** Every field and option a SAML row lacks, named in one error — or nothing. */
608
+ function samlRefusal(check) {
609
+ const { destination, grant, means, client } = check;
610
+ const lacking = missing(means, [
611
+ 'samlIdpSsoUrl',
612
+ 'samlSpEntityId',
613
+ 'samlIdpEntityId',
614
+ ]);
615
+ const certificates = means.samlIdpCertificates;
616
+ if (!Array.isArray(certificates) ||
617
+ certificates.length === 0 ||
618
+ !certificates.every(present)) {
619
+ lacking.push('samlIdpCertificates');
620
+ }
621
+ if (!trustShaped(means.samlIdpInitiated, 'boolean')) {
622
+ lacking.push('samlIdpInitiated');
623
+ }
624
+ if (grant === 'saml2_bearer' && client !== undefined) {
625
+ lacking.push(...missing(client, ['uaaUrl', 'uaaClientId']));
626
+ }
627
+ if (!check.authorization)
628
+ lacking.push('authorization');
629
+ if (grant === 'saml2_pure' && !check.samlCookies) {
630
+ lacking.push('samlCookies');
631
+ }
632
+ if (!check.assertionReplayStore)
633
+ lacking.push('assertionReplayStore');
634
+ lacking.push(...optionsLacking(check));
635
+ if (lacking.length === 0)
636
+ return undefined;
637
+ return clientRowError(destination, lacking, `a saml destination with grantType ${grant} lacks what its grant needs`, grant === 'saml2_bearer' && noClientHint(check.clientAuthenticated, client));
638
+ }
373
639
  /**
374
640
  * `saml` / `saml2_pure` → `Saml2PureProvider`, `saml2_bearer` →
375
641
  * `Saml2BearerProvider`, each validating the assertion before anything uses
@@ -384,39 +650,24 @@ function isSamlGrant(grant) {
384
650
  * `saml2_bearer` with the stored token, its refresh token and expiry, and
385
651
  * takes the client (`uaaUrl`, `uaaClientId`; without a strategy,
386
652
  * `uaaClientSecret` `''` a public client; on the strategy path the identity
387
- * alone, no secret) and `samlTokenUrl` when stated.
653
+ * alone, no secret) and `samlTokenUrl` when stated. Both take the consumer's
654
+ * renewal for their grant and the broker's persistence.
388
655
  *
389
- * Every field and option the row lacks is named in one error, before any
390
- * collaborator is called; so is a `samlClockSkewMs` that is not a whole number
391
- * of milliseconds, and a certificate the validator cannot read — by name, never
392
- * the value.
656
+ * Every field and option the row lacks is named in one error
657
+ * (`samlRefusal`), before any collaborator is called; so is a
658
+ * `samlClockSkewMs` that is not a whole number of milliseconds, and a
659
+ * certificate the validator cannot read — by name, never the value, the
660
+ * validator's error carried.
393
661
  */
394
662
  function samlProvider(row) {
395
663
  const { destination, grant, means, client } = row;
396
- const lacking = missing(means, [
397
- 'samlIdpSsoUrl',
398
- 'samlSpEntityId',
399
- 'samlIdpEntityId',
400
- ]);
664
+ const refusal = samlRefusal({
665
+ ...row,
666
+ clientAuthenticated: !!row.clientAuthentication,
667
+ });
668
+ if (refusal)
669
+ throw refusal;
401
670
  const certificates = means.samlIdpCertificates;
402
- if (!Array.isArray(certificates) ||
403
- certificates.length === 0 ||
404
- !certificates.every(present)) {
405
- lacking.push('samlIdpCertificates');
406
- }
407
- if (grant === 'saml2_bearer') {
408
- lacking.push(...missing(client, ['uaaUrl', 'uaaClientId']));
409
- }
410
- if (!row.authorization)
411
- lacking.push('authorization');
412
- if (grant === 'saml2_pure' && !row.samlCookies)
413
- lacking.push('samlCookies');
414
- if (!row.assertionReplayStore)
415
- lacking.push('assertionReplayStore');
416
- if (lacking.length > 0) {
417
- throw clientRowError(destination, lacking, `a saml destination with grantType ${grant} lacks what its grant needs`, grant === 'saml2_bearer' &&
418
- noClientHint(row.clientAuthentication, client));
419
- }
420
671
  const skew = means.samlClockSkewMs;
421
672
  if (skew !== undefined && !(Number.isInteger(skew) && skew >= 0)) {
422
673
  throw new DestinationConfigError_1.DestinationConfigError(destination, ['samlClockSkewMs'], 'samlClockSkewMs is not a whole, non-negative number of milliseconds');
@@ -427,18 +678,20 @@ function samlProvider(row) {
427
678
  : auth_providers_1.createSignedAssertionValidator;
428
679
  let assertionValidator;
429
680
  try {
430
- assertionValidator = createValidator({
681
+ assertionValidator = createValidator((0, contractShape_1.asContract)({
431
682
  idpCertificates: certificates,
432
683
  clockSkewMs: skew,
433
684
  replayStore,
434
- });
685
+ }));
435
686
  }
436
- catch {
437
- // The validator's own message may quote what it could not read; the
438
- // error names the field only.
439
- throw new DestinationConfigError_1.DestinationConfigError(destination, ['samlIdpCertificates'], 'a SAML signing certificate the destination states is not a valid X.509 certificate');
687
+ catch (thrown) {
688
+ // The field is named, never its value; the validator's error is carried
689
+ // as auth-errors reads it.
690
+ throw new DestinationConfigError_1.DestinationConfigError(destination, ['samlIdpCertificates'], 'a SAML signing certificate the destination states is not a valid X.509 certificate', (0, auth_errors_1.readFailure)(thrown, 'validating-assertion'));
440
691
  }
441
- const common = {
692
+ const renewal = renewalFor(destination, grant, row.renewal);
693
+ const authorization = row.authorization(destination, grant);
694
+ const common = () => ({
442
695
  idpSsoUrl: means.samlIdpSsoUrl,
443
696
  spEntityId: means.samlSpEntityId,
444
697
  acsUrl: stated(means.samlAcsUrl),
@@ -446,28 +699,41 @@ function samlProvider(row) {
446
699
  idpEntityId: means.samlIdpEntityId,
447
700
  idpInitiated: means.samlIdpInitiated === true,
448
701
  assertionValidator,
449
- authorization: row.authorization(destination, grant),
702
+ authorization,
450
703
  logger: row.logger,
451
- onTokens: row.onTokens,
452
- };
704
+ authDebug: row.authDebug,
705
+ renewal,
706
+ persistence: persistenceFor(row),
707
+ });
453
708
  if (grant === 'saml2_pure') {
454
709
  const secret = row.secret;
455
- return new auth_providers_1.Saml2PureProvider({
456
- ...common,
457
- cookieProvider: row.samlCookies(destination),
458
- accessToken: stated(secret?.sessionCookies),
459
- expiresAt: typeof secret?.expiresAt === 'number' ? secret.expiresAt : undefined,
460
- });
461
- }
462
- return new auth_providers_1.Saml2BearerProvider({
463
- ...common,
464
- tokenUrl: stated(means.samlTokenUrl),
465
- uaaUrl: client?.uaaUrl,
466
- clientId: client?.uaaClientId,
467
- clientSecret: row.clientAuthentication
468
- ? undefined
469
- : stated(secretOf(client)),
470
- ...authenticatedBy(row.clientAuthentication),
471
- ...tokenSeed(row.secret),
472
- });
710
+ const cookieProvider = row.samlCookies(destination);
711
+ // The cookies and their expiry: no refresh token.
712
+ return {
713
+ provider: constructed(destination, SAML_FIELDS, () => new auth_providers_1.Saml2PureProvider((0, contractShape_1.asContract)({
714
+ ...common(),
715
+ cookieProvider,
716
+ accessToken: stated(secret?.sessionCookies),
717
+ expiresAt: typeof secret?.expiresAt === 'number'
718
+ ? secret.expiresAt
719
+ : undefined,
720
+ }))),
721
+ seededRefreshToken: undefined,
722
+ };
723
+ }
724
+ const seed = tokenSeed(row.secret);
725
+ return {
726
+ provider: constructed(destination, SAML_FIELDS, () => new auth_providers_1.Saml2BearerProvider((0, contractShape_1.asContract)({
727
+ ...common(),
728
+ tokenUrl: stated(means.samlTokenUrl),
729
+ uaaUrl: client?.uaaUrl,
730
+ clientId: client?.uaaClientId,
731
+ clientSecret: row.clientAuthentication
732
+ ? undefined
733
+ : stated(secretOf(client)),
734
+ ...authenticatedBy(row.clientAuthentication),
735
+ ...seed,
736
+ }))),
737
+ seededRefreshToken: seed.refreshToken,
738
+ };
473
739
  }