@mcp-abap-adt/auth-broker 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +273 -1
- package/README.md +1028 -908
- package/dist/AuthBroker.d.ts +353 -99
- package/dist/AuthBroker.d.ts.map +1 -1
- package/dist/AuthBroker.js +806 -258
- package/dist/DestinationConfigError.d.ts +35 -6
- package/dist/DestinationConfigError.d.ts.map +1 -1
- package/dist/DestinationConfigError.js +51 -9
- package/dist/SessionWriter.d.ts +70 -33
- package/dist/SessionWriter.d.ts.map +1 -1
- package/dist/SessionWriter.js +118 -94
- package/dist/binding.d.ts +137 -64
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +330 -131
- package/dist/bindingOf.d.ts +33 -14
- package/dist/bindingOf.d.ts.map +1 -1
- package/dist/bindingOf.js +50 -20
- package/dist/buildIdentity.d.ts +137 -0
- package/dist/buildIdentity.d.ts.map +1 -0
- package/dist/buildIdentity.js +393 -0
- package/dist/clientAuthentication.d.ts +124 -0
- package/dist/clientAuthentication.d.ts.map +1 -0
- package/dist/clientAuthentication.js +218 -0
- package/dist/contractShape.d.ts +24 -0
- package/dist/contractShape.d.ts.map +1 -0
- package/dist/contractShape.js +24 -0
- package/dist/destinations.d.ts +174 -53
- package/dist/destinations.d.ts.map +1 -1
- package/dist/destinations.js +460 -150
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/quietLogger.d.ts +24 -0
- package/dist/quietLogger.d.ts.map +1 -0
- package/dist/quietLogger.js +63 -0
- package/package.json +9 -7
package/dist/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;
|
|
@@ -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 (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
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 (
|
|
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
|
-
*
|
|
149
|
-
* `
|
|
150
|
-
*
|
|
151
|
-
*
|
|
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
|
|
160
|
-
const { destination, grant, client,
|
|
352
|
+
function uaaRefusal(check) {
|
|
353
|
+
const { destination, grant, client, clientAuthenticated } = check;
|
|
161
354
|
const lacking = [];
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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' && !
|
|
366
|
+
if (grant !== 'client_credentials' && !check.authorization) {
|
|
170
367
|
lacking.push('authorization');
|
|
171
368
|
}
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
178
|
-
? client
|
|
398
|
+
const clientSecret = !clientAuthentication && present(secretOf(client))
|
|
399
|
+
? secretOf(client)
|
|
179
400
|
: undefined;
|
|
180
|
-
const
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
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
|
|
442
|
+
clientSecret,
|
|
198
443
|
authorization,
|
|
199
444
|
...seed,
|
|
200
445
|
logger: row.logger,
|
|
201
|
-
...hooks,
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
260
|
-
const { destination, grant, means, client } =
|
|
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' && !
|
|
511
|
+
if (grant === 'oidc_authorization_code' && !check.oidcAuthorization) {
|
|
276
512
|
lacking.push('oidcAuthorization');
|
|
277
513
|
}
|
|
278
|
-
if (grant === 'device_code' && !
|
|
514
|
+
if (grant === 'device_code' && !check.deviceCodePresenter) {
|
|
279
515
|
lacking.push('deviceCodePresenter');
|
|
280
516
|
}
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
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:
|
|
554
|
+
clientSecret: row.clientAuthentication
|
|
555
|
+
? undefined
|
|
556
|
+
: stated(secretOf(client)),
|
|
557
|
+
...authenticatedBy(row.clientAuthentication),
|
|
291
558
|
tokenEndpoint: stated(means.oidcTokenEndpoint),
|
|
292
|
-
...
|
|
559
|
+
...seed,
|
|
293
560
|
logger: row.logger,
|
|
294
|
-
|
|
295
|
-
|
|
561
|
+
authDebug: row.authDebug,
|
|
562
|
+
renewal,
|
|
563
|
+
persistence: persistenceFor(row),
|
|
564
|
+
});
|
|
296
565
|
switch (grant) {
|
|
297
|
-
case 'oidc_authorization_code':
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
case 'device_code':
|
|
306
|
-
|
|
307
|
-
|
|
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
|
|
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`;
|
|
347
|
-
*
|
|
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
|
|
350
|
-
* collaborator is called; so is a
|
|
351
|
-
*
|
|
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
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
|
397
|
-
//
|
|
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
|
|
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
|
|
702
|
+
authorization,
|
|
409
703
|
logger: row.logger,
|
|
410
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
}
|