@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.
- package/CHANGELOG.md +138 -1
- package/README.md +960 -1214
- package/dist/AuthBroker.d.ts +294 -110
- package/dist/AuthBroker.d.ts.map +1 -1
- package/dist/AuthBroker.js +726 -351
- package/dist/DestinationConfigError.d.ts +35 -6
- package/dist/DestinationConfigError.d.ts.map +1 -1
- package/dist/DestinationConfigError.js +51 -9
- package/dist/SessionWriter.d.ts +70 -33
- package/dist/SessionWriter.d.ts.map +1 -1
- package/dist/SessionWriter.js +118 -94
- package/dist/binding.d.ts +125 -74
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +320 -137
- package/dist/bindingOf.d.ts +33 -15
- package/dist/bindingOf.d.ts.map +1 -1
- package/dist/bindingOf.js +50 -20
- package/dist/buildIdentity.d.ts +137 -0
- package/dist/buildIdentity.d.ts.map +1 -0
- package/dist/buildIdentity.js +393 -0
- package/dist/clientAuthentication.d.ts +21 -6
- package/dist/clientAuthentication.d.ts.map +1 -1
- package/dist/clientAuthentication.js +55 -55
- package/dist/contractShape.d.ts +24 -0
- package/dist/contractShape.d.ts.map +1 -0
- package/dist/contractShape.js +24 -0
- package/dist/destinations.d.ts +149 -53
- package/dist/destinations.d.ts.map +1 -1
- package/dist/destinations.js +426 -160
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/quietLogger.d.ts +24 -0
- package/dist/quietLogger.d.ts.map +1 -0
- package/dist/quietLogger.js +63 -0
- package/package.json +8 -7
package/dist/destinations.js
CHANGED
|
@@ -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(
|
|
83
|
-
return !
|
|
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 (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
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 (
|
|
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
|
-
*
|
|
175
|
-
* `
|
|
176
|
-
*
|
|
177
|
-
*
|
|
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
|
|
191
|
-
const { destination, grant, client,
|
|
352
|
+
function uaaRefusal(check) {
|
|
353
|
+
const { destination, grant, client, clientAuthenticated } = check;
|
|
192
354
|
const lacking = [];
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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' && !
|
|
366
|
+
if (grant !== 'client_credentials' && !check.authorization) {
|
|
203
367
|
lacking.push('authorization');
|
|
204
368
|
}
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
214
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
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
|
-
|
|
442
|
+
clientSecret,
|
|
234
443
|
authorization,
|
|
235
444
|
...seed,
|
|
236
445
|
logger: row.logger,
|
|
237
|
-
...hooks,
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
296
|
-
const { destination, grant, means, client } =
|
|
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' && !
|
|
511
|
+
if (grant === 'oidc_authorization_code' && !check.oidcAuthorization) {
|
|
312
512
|
lacking.push('oidcAuthorization');
|
|
313
513
|
}
|
|
314
|
-
if (grant === 'device_code' && !
|
|
514
|
+
if (grant === 'device_code' && !check.deviceCodePresenter) {
|
|
315
515
|
lacking.push('deviceCodePresenter');
|
|
316
516
|
}
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
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
|
-
...
|
|
559
|
+
...seed,
|
|
332
560
|
logger: row.logger,
|
|
333
|
-
|
|
334
|
-
|
|
561
|
+
authDebug: row.authDebug,
|
|
562
|
+
renewal,
|
|
563
|
+
persistence: persistenceFor(row),
|
|
564
|
+
});
|
|
335
565
|
switch (grant) {
|
|
336
|
-
case 'oidc_authorization_code':
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
case 'device_code':
|
|
345
|
-
|
|
346
|
-
|
|
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
|
|
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
|
|
390
|
-
* collaborator is called; so is a
|
|
391
|
-
*
|
|
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
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
|
438
|
-
//
|
|
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
|
|
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
|
|
702
|
+
authorization,
|
|
450
703
|
logger: row.logger,
|
|
451
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
}
|