@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.
@@ -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;
@@ -64,6 +110,32 @@ function statedGrant(destination, authType, means) {
64
110
  }
65
111
  return grantType;
66
112
  }
113
+ /** The secret a row's client holds; an identity holds none. */
114
+ function secretOf(client) {
115
+ return client && 'uaaClientSecret' in client
116
+ ? client.uaaClientSecret
117
+ : undefined;
118
+ }
119
+ /** Added to a row's refusal when it has no client and no strategy was given. */
120
+ const CERTIFICATE_HINT = 'a certificate client needs a clientAuthentication strategy';
121
+ /**
122
+ * Whether a client row's refusal gets the hint: no strategy, and no client
123
+ * at all — the key store answered none, or one without a client id. A
124
+ * client that lacks only its secret or its URL is a secret client with a
125
+ * gap, and gets 4.0.0's words alone. Decided from what was read, never by
126
+ * asking the store about a certificate.
127
+ */
128
+ function noClientHint(clientAuthenticated, client) {
129
+ return !clientAuthenticated && !present(client?.uaaClientId);
130
+ }
131
+ /** A client row's refusal: 4.0.0's words, and the hint when `hinted`. */
132
+ function clientRowError(destination, lacking, reason, hinted) {
133
+ return new DestinationConfigError_1.DestinationConfigError(destination, lacking, hinted ? `${reason}; ${CERTIFICATE_HINT}` : reason);
134
+ }
135
+ /** `clientAuthentication` for a provider's config: present only when given. */
136
+ function authenticatedBy(clientAuthentication) {
137
+ return clientAuthentication ? { clientAuthentication } : {};
138
+ }
67
139
  /** The names of the fields among `names` that `source` lacks or holds as `''`. */
68
140
  function missing(source, names) {
69
141
  return names.filter((name) => !present(source?.[name]));
@@ -94,30 +166,55 @@ function sncProvider(destination, means, logger) {
94
166
  throw new DestinationConfigError_1.DestinationConfigError(destination, ['sncPartnerName'], 'an snc destination needs the system’s SNC name');
95
167
  }
96
168
  try {
97
- return auth_providers_1.SncLogonProvider.forSecureLoginClient({
169
+ return auth_providers_1.SncLogonProvider.forSecureLoginClient((0, contractShape_1.asContract)({
98
170
  partnerName: means.sncPartnerName,
99
171
  qop: present(means.sncQop) ? means.sncQop : undefined,
100
172
  sncLib: present(means.sncLib) ? means.sncLib : undefined,
101
173
  myName: present(means.sncMyName) ? means.sncMyName : undefined,
102
174
  logger,
103
- });
175
+ }));
104
176
  }
105
- catch (error) {
106
- if (error instanceof auth_providers_1.ValidationError) {
107
- throw new DestinationConfigError_1.DestinationConfigError(destination, (error.missingFields ?? []).map((field) => SNC_FIELDS[field] ?? field), 'the SNC provider refused the destination’s SNC settings');
108
- }
109
- 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);
185
+ }
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);
110
205
  }
206
+ return names;
111
207
  }
112
208
  /**
113
209
  * `none` → the credential handed over, from the session: the only way in, so
114
210
  * required. Read from the secret alone — the means are not a seed.
115
211
  *
116
212
  * The broker did not obtain it and cannot obtain it again, so a binding that
117
- * does not match is refused, never discarded: `issuedFor`
118
- * must always equal the destination's resource — it is what stops the
119
- * credential going to another one; `issuedBy` is compared only when the means
120
- * 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`.
121
218
  */
122
219
  function handedOverProvider(destination, authType, secret, binding) {
123
220
  const credential = authType === 'jwt' ? secret?.authorizationToken : secret?.sessionCookies;
@@ -129,9 +226,8 @@ function handedOverProvider(destination, authType, secret, binding) {
129
226
  const unbound = [];
130
227
  if (!(0, binding_1.sameResource)(secret, binding))
131
228
  unbound.push('issuedFor');
132
- if (binding.issuerStated && !(0, binding_1.sameIssuer)(secret, binding)) {
229
+ if (!(0, binding_1.sameRecord)(secret, binding))
133
230
  unbound.push('issuedBy');
134
- }
135
231
  if (unbound.length > 0) {
136
232
  throw new DestinationConfigError_1.DestinationConfigError(destination, unbound, 'the credential in the session is not bound to this destination’s means');
137
233
  }
@@ -139,77 +235,218 @@ function handedOverProvider(destination, authType, secret, binding) {
139
235
  ? auth_providers_1.TokenAuthProvider.fixed(credential)
140
236
  : new auth_providers_1.SamlAuthProvider(credential);
141
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
+ };
142
340
  function isUaaGrant(grant) {
143
341
  return (grant === 'authorization_code' ||
144
342
  grant === 'client_credentials' ||
145
343
  grant === 'passcode');
146
344
  }
147
345
  /**
148
- * `jwt` / `authorization_code` → `AuthorizationCodeProvider`,
149
- * `client_credentials` → `ClientCredentialsProvider`, `passcode` →
150
- * `UaaPasscodeProvider`: the client from the key store, the seed from the
151
- * session (not for `client_credentials`, whose row takes the client alone).
152
- *
153
- * Every field and option the row lacks is named in one error, before the
154
- * consumer's `authorization` is called. `serviceUrl` is not among them: the
155
- * providers read the client and `uaaUrl` only. `uaaClientSecret: ''` is a public
156
- * client: `passcode` takes it as no secret; the other two rows' providers
157
- * require a secret, so for them `''` is missing.
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.
158
351
  */
159
- function uaaProvider(row) {
160
- const { destination, grant, client, secret } = row;
352
+ function uaaRefusal(check) {
353
+ const { destination, grant, client, clientAuthenticated } = check;
161
354
  const lacking = [];
162
- if (!present(client?.uaaUrl))
163
- lacking.push('uaaUrl');
164
- if (!present(client?.uaaClientId))
165
- lacking.push('uaaClientId');
166
- if (grant !== 'passcode' && !present(client?.uaaClientSecret)) {
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 &&
362
+ grant !== 'passcode' &&
363
+ !present(secretOf(client ?? null))) {
167
364
  lacking.push('uaaClientSecret');
168
365
  }
169
- if (grant !== 'client_credentials' && !row.authorization) {
366
+ if (grant !== 'client_credentials' && !check.authorization) {
170
367
  lacking.push('authorization');
171
368
  }
172
- if (lacking.length > 0) {
173
- throw new DestinationConfigError_1.DestinationConfigError(destination, lacking, `a jwt destination with grantType ${grant} lacks what its grant needs`);
174
- }
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;
175
396
  const uaaUrl = client?.uaaUrl;
176
397
  const clientId = client?.uaaClientId;
177
- const clientSecret = present(client?.uaaClientSecret)
178
- ? client.uaaClientSecret
398
+ const clientSecret = !clientAuthentication && present(secretOf(client))
399
+ ? secretOf(client)
179
400
  : undefined;
180
- const hooks = { onTokens: row.onTokens };
401
+ const renewal = renewalFor(destination, grant, row.renewal);
402
+ const hooks = () => ({
403
+ renewal,
404
+ persistence: persistenceFor(row),
405
+ ...authenticatedBy(clientAuthentication),
406
+ authDebug: row.authDebug,
407
+ });
181
408
  if (grant === 'client_credentials') {
182
- return new auth_providers_1.ClientCredentialsProvider({
183
- uaaUrl,
184
- clientId,
185
- clientSecret: clientSecret,
186
- logger: row.logger,
187
- ...hooks,
188
- });
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
+ };
189
420
  }
190
421
  const seed = tokenSeed(secret);
191
422
  // Checked above: the rows that reach here need it.
192
423
  const authorization = row.authorization(destination, grant);
193
424
  if (grant === 'authorization_code') {
194
- 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)({
195
440
  uaaUrl,
196
441
  clientId,
197
- clientSecret: clientSecret,
442
+ clientSecret,
198
443
  authorization,
199
444
  ...seed,
200
445
  logger: row.logger,
201
- ...hooks,
202
- });
203
- }
204
- return new auth_providers_1.UaaPasscodeProvider({
205
- uaaUrl,
206
- clientId,
207
- clientSecret,
208
- authorization,
209
- ...seed,
210
- logger: row.logger,
211
- ...hooks,
212
- });
446
+ ...hooks(),
447
+ }))),
448
+ seededRefreshToken: seed.refreshToken,
449
+ };
213
450
  }
214
451
  /** The seed of a token provider: the stored token, its refresh token and expiry. */
215
452
  function tokenSeed(secret) {
@@ -244,93 +481,161 @@ const OIDC_ENDPOINTS = {
244
481
  token_exchange: ['oidcTokenEndpoint'],
245
482
  };
246
483
  /**
247
- * `jwt` / `oidc_authorization_code` → `OidcBrowserProvider`, `device_code` →
248
- * `OidcDeviceFlowProvider`, `password` → `OidcPasswordProvider`,
249
- * `token_exchange` → `OidcTokenExchangeProvider`: the client id (and secret,
250
- * `''` being a public client) from the key store's client, the grant's data
251
- * from its `oidc*` fields, the seed from the session.
252
- *
484
+ * Every field and option an OIDC row lacks, named in one error — or nothing.
253
485
  * The endpoints: `oidcIssuerUrl`, from which the provider discovers them, or
254
486
  * every explicit endpoint the row uses — without either the error names the
255
- * issuer and the endpoints missing. Every field and option the row lacks is
256
- * named in one error, before any collaborator is called. `token_exchange`
257
- * takes one scope string: the stored scopes joined by a space.
487
+ * issuer and the endpoints missing.
258
488
  */
259
- function oidcProvider(row) {
260
- const { destination, grant, means, client } = row;
489
+ function oidcRefusal(check) {
490
+ const { destination, grant, means, client } = check;
261
491
  const lacking = [];
262
- if (!present(client?.uaaClientId))
492
+ if (client !== undefined && !present(client?.uaaClientId)) {
263
493
  lacking.push('uaaClientId');
494
+ }
264
495
  if (!present(means.oidcIssuerUrl)) {
265
496
  const endpoints = OIDC_ENDPOINTS[grant].filter((name) => !present(means[name]));
266
497
  if (endpoints.length > 0)
267
498
  lacking.push('oidcIssuerUrl', ...endpoints);
268
499
  }
500
+ if (!trustShaped(means.oidcScopes, 'strings'))
501
+ lacking.push('oidcScopes');
269
502
  if (grant === 'password') {
270
503
  lacking.push(...missing(means, ['username', 'password']));
271
504
  }
272
505
  if (grant === 'token_exchange') {
273
506
  lacking.push(...missing(means, ['oidcSubjectToken', 'oidcSubjectTokenType']));
507
+ if (!trustShaped(means.oidcActorTokenType, 'string')) {
508
+ lacking.push('oidcActorTokenType');
509
+ }
274
510
  }
275
- if (grant === 'oidc_authorization_code' && !row.oidcAuthorization) {
511
+ if (grant === 'oidc_authorization_code' && !check.oidcAuthorization) {
276
512
  lacking.push('oidcAuthorization');
277
513
  }
278
- if (grant === 'device_code' && !row.deviceCodePresenter) {
514
+ if (grant === 'device_code' && !check.deviceCodePresenter) {
279
515
  lacking.push('deviceCodePresenter');
280
516
  }
281
- if (lacking.length > 0) {
282
- throw new DestinationConfigError_1.DestinationConfigError(destination, lacking, `a jwt destination with grantType ${grant} lacks what its grant needs`);
283
- }
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;
284
542
  const scopes = Array.isArray(means.oidcScopes) && means.oidcScopes.length > 0
285
543
  ? means.oidcScopes
286
544
  : undefined;
287
- 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 = () => ({
288
552
  issuerUrl: stated(means.oidcIssuerUrl),
289
553
  clientId: client?.uaaClientId,
290
- clientSecret: stated(client?.uaaClientSecret),
554
+ clientSecret: row.clientAuthentication
555
+ ? undefined
556
+ : stated(secretOf(client)),
557
+ ...authenticatedBy(row.clientAuthentication),
291
558
  tokenEndpoint: stated(means.oidcTokenEndpoint),
292
- ...tokenSeed(row.secret),
559
+ ...seed,
293
560
  logger: row.logger,
294
- onTokens: row.onTokens,
295
- };
561
+ authDebug: row.authDebug,
562
+ renewal,
563
+ persistence: persistenceFor(row),
564
+ });
296
565
  switch (grant) {
297
- case 'oidc_authorization_code':
298
- return new auth_providers_1.OidcBrowserProvider({
299
- ...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(),
300
571
  scopes,
301
572
  authorizationEndpoint: stated(means.oidcAuthorizationEndpoint),
302
- // Checked above.
303
- authorization: row.oidcAuthorization(destination),
304
- });
305
- case 'device_code':
306
- return new auth_providers_1.OidcDeviceFlowProvider({
307
- ...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(),
308
580
  scopes,
309
581
  deviceAuthorizationEndpoint: stated(means.oidcDeviceAuthorizationEndpoint),
310
- presenter: row.deviceCodePresenter(destination),
311
- });
582
+ presenter,
583
+ }))));
584
+ }
312
585
  case 'password':
313
- return new auth_providers_1.OidcPasswordProvider({
314
- ...common,
586
+ return built(constructed(destination, OIDC_FIELDS, () => new auth_providers_1.OidcPasswordProvider((0, contractShape_1.asContract)({
587
+ ...common(),
315
588
  scopes,
316
589
  username: means.username,
317
590
  password: means.password,
318
- });
591
+ }))));
319
592
  case 'token_exchange':
320
- return new auth_providers_1.OidcTokenExchangeProvider({
321
- ...common,
593
+ return built(constructed(destination, OIDC_FIELDS, () => new auth_providers_1.OidcTokenExchangeProvider((0, contractShape_1.asContract)({
594
+ ...common(),
322
595
  scope: scopes?.join(' '),
323
596
  subjectToken: means.oidcSubjectToken,
324
597
  subjectTokenType: means.oidcSubjectTokenType,
325
598
  audience: stated(means.oidcAudience),
326
599
  actorToken: stated(means.oidcActorToken),
327
600
  actorTokenType: stated(means.oidcActorTokenType),
328
- });
601
+ }))));
329
602
  }
330
603
  }
331
604
  function isSamlGrant(grant) {
332
605
  return grant === 'saml2_pure' || grant === 'saml2_bearer';
333
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
+ }
334
639
  /**
335
640
  * `saml` / `saml2_pure` → `Saml2PureProvider`, `saml2_bearer` →
336
641
  * `Saml2BearerProvider`, each validating the assertion before anything uses
@@ -343,39 +648,26 @@ function isSamlGrant(grant) {
343
648
  *
344
649
  * `saml2_pure` is seeded with the stored cookies and `expiresAt`;
345
650
  * `saml2_bearer` with the stored token, its refresh token and expiry, and
346
- * takes the client (`uaaUrl`, `uaaClientId`; `uaaClientSecret` `''` a public
347
- * client) and `samlTokenUrl` when stated.
651
+ * takes the client (`uaaUrl`, `uaaClientId`; without a strategy,
652
+ * `uaaClientSecret` `''` a public client; on the strategy path the identity
653
+ * alone, no secret) and `samlTokenUrl` when stated. Both take the consumer's
654
+ * renewal for their grant and the broker's persistence.
348
655
  *
349
- * Every field and option the row lacks is named in one error, before any
350
- * collaborator is called; so is a `samlClockSkewMs` that is not a whole number
351
- * of milliseconds, and a certificate the validator cannot read — by name, never
352
- * 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.
353
661
  */
354
662
  function samlProvider(row) {
355
663
  const { destination, grant, means, client } = row;
356
- const lacking = missing(means, [
357
- 'samlIdpSsoUrl',
358
- 'samlSpEntityId',
359
- 'samlIdpEntityId',
360
- ]);
664
+ const refusal = samlRefusal({
665
+ ...row,
666
+ clientAuthenticated: !!row.clientAuthentication,
667
+ });
668
+ if (refusal)
669
+ throw refusal;
361
670
  const certificates = means.samlIdpCertificates;
362
- if (!Array.isArray(certificates) ||
363
- certificates.length === 0 ||
364
- !certificates.every(present)) {
365
- lacking.push('samlIdpCertificates');
366
- }
367
- if (grant === 'saml2_bearer') {
368
- lacking.push(...missing(client, ['uaaUrl', 'uaaClientId']));
369
- }
370
- if (!row.authorization)
371
- lacking.push('authorization');
372
- if (grant === 'saml2_pure' && !row.samlCookies)
373
- lacking.push('samlCookies');
374
- if (!row.assertionReplayStore)
375
- lacking.push('assertionReplayStore');
376
- if (lacking.length > 0) {
377
- throw new DestinationConfigError_1.DestinationConfigError(destination, lacking, `a saml destination with grantType ${grant} lacks what its grant needs`);
378
- }
379
671
  const skew = means.samlClockSkewMs;
380
672
  if (skew !== undefined && !(Number.isInteger(skew) && skew >= 0)) {
381
673
  throw new DestinationConfigError_1.DestinationConfigError(destination, ['samlClockSkewMs'], 'samlClockSkewMs is not a whole, non-negative number of milliseconds');
@@ -386,18 +678,20 @@ function samlProvider(row) {
386
678
  : auth_providers_1.createSignedAssertionValidator;
387
679
  let assertionValidator;
388
680
  try {
389
- assertionValidator = createValidator({
681
+ assertionValidator = createValidator((0, contractShape_1.asContract)({
390
682
  idpCertificates: certificates,
391
683
  clockSkewMs: skew,
392
684
  replayStore,
393
- });
685
+ }));
394
686
  }
395
- catch {
396
- // The validator's own message may quote what it could not read; the
397
- // error names the field only.
398
- 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'));
399
691
  }
400
- const common = {
692
+ const renewal = renewalFor(destination, grant, row.renewal);
693
+ const authorization = row.authorization(destination, grant);
694
+ const common = () => ({
401
695
  idpSsoUrl: means.samlIdpSsoUrl,
402
696
  spEntityId: means.samlSpEntityId,
403
697
  acsUrl: stated(means.samlAcsUrl),
@@ -405,25 +699,41 @@ function samlProvider(row) {
405
699
  idpEntityId: means.samlIdpEntityId,
406
700
  idpInitiated: means.samlIdpInitiated === true,
407
701
  assertionValidator,
408
- authorization: row.authorization(destination, grant),
702
+ authorization,
409
703
  logger: row.logger,
410
- onTokens: row.onTokens,
411
- };
704
+ authDebug: row.authDebug,
705
+ renewal,
706
+ persistence: persistenceFor(row),
707
+ });
412
708
  if (grant === 'saml2_pure') {
413
709
  const secret = row.secret;
414
- return new auth_providers_1.Saml2PureProvider({
415
- ...common,
416
- cookieProvider: row.samlCookies(destination),
417
- accessToken: stated(secret?.sessionCookies),
418
- expiresAt: typeof secret?.expiresAt === 'number' ? secret.expiresAt : undefined,
419
- });
420
- }
421
- return new auth_providers_1.Saml2BearerProvider({
422
- ...common,
423
- tokenUrl: stated(means.samlTokenUrl),
424
- uaaUrl: client?.uaaUrl,
425
- clientId: client?.uaaClientId,
426
- clientSecret: stated(client?.uaaClientSecret),
427
- ...tokenSeed(row.secret),
428
- });
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
+ };
429
739
  }