@boxyhq/saml-jackson 1.2.1 → 1.3.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/dist/controller/admin.d.ts +4 -4
- package/dist/controller/admin.js +6 -6
- package/dist/controller/api.d.ts +448 -204
- package/dist/controller/api.js +547 -376
- package/dist/controller/connection/oidc.d.ts +18 -0
- package/dist/controller/connection/oidc.js +145 -0
- package/dist/controller/connection/saml.d.ts +14 -0
- package/dist/controller/connection/saml.js +168 -0
- package/dist/controller/logout.d.ts +3 -3
- package/dist/controller/logout.js +14 -14
- package/dist/controller/oauth.d.ts +26 -8
- package/dist/controller/oauth.js +368 -118
- package/dist/controller/utils.d.ts +10 -2
- package/dist/controller/utils.js +88 -1
- package/dist/directory-sync/DirectoryUsers.js +4 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +26 -14
- package/dist/loadConnection.d.ts +3 -0
- package/dist/{read-config.js → loadConnection.js} +13 -12
- package/dist/opentelemetry/metrics.js +12 -12
- package/dist/saml/x509.d.ts +4 -4
- package/dist/saml/x509.js +38 -42
- package/dist/typings.d.ts +110 -34
- package/package.json +14 -14
- package/dist/read-config.d.ts +0 -3
package/dist/controller/oauth.js
CHANGED
@@ -39,6 +39,7 @@ exports.OAuthController = void 0;
|
|
39
39
|
const crypto_1 = __importDefault(require("crypto"));
|
40
40
|
const util_1 = require("util");
|
41
41
|
const zlib_1 = require("zlib");
|
42
|
+
const openid_client_1 = require("openid-client");
|
42
43
|
const jose = __importStar(require("jose"));
|
43
44
|
const dbutils = __importStar(require("../db/utils"));
|
44
45
|
const metrics = __importStar(require("../opentelemetry/metrics"));
|
@@ -49,8 +50,9 @@ const allowed = __importStar(require("./oauth/allowed"));
|
|
49
50
|
const codeVerifier = __importStar(require("./oauth/code-verifier"));
|
50
51
|
const redirect = __importStar(require("./oauth/redirect"));
|
51
52
|
const utils_1 = require("./utils");
|
53
|
+
const x509_1 = __importDefault(require("../saml/x509"));
|
52
54
|
const deflateRawAsync = (0, util_1.promisify)(zlib_1.deflateRaw);
|
53
|
-
const
|
55
|
+
const validateSAMLResponse = (rawResponse, validateOpts) => __awaiter(void 0, void 0, void 0, function* () {
|
54
56
|
const profile = yield saml20_1.default.validate(rawResponse, validateOpts);
|
55
57
|
if (profile && profile.claims) {
|
56
58
|
// we map claims to our attributes id, email, firstName, lastName where possible. We also map original claims to raw
|
@@ -83,31 +85,37 @@ function getScopeValues(scope) {
|
|
83
85
|
return typeof scope === 'string' ? scope.split(' ').filter((s) => s.length > 0) : [];
|
84
86
|
}
|
85
87
|
class OAuthController {
|
86
|
-
constructor({
|
87
|
-
this.
|
88
|
+
constructor({ connectionStore, sessionStore, codeStore, tokenStore, opts }) {
|
89
|
+
this.connectionStore = connectionStore;
|
88
90
|
this.sessionStore = sessionStore;
|
89
91
|
this.codeStore = codeStore;
|
90
92
|
this.tokenStore = tokenStore;
|
91
93
|
this.opts = opts;
|
92
94
|
}
|
93
|
-
|
94
|
-
if (
|
95
|
+
resolveMultipleConnectionMatches(connections, idp_hint, originalParams, isIdpFlow = false) {
|
96
|
+
if (connections.length > 1) {
|
95
97
|
if (idp_hint) {
|
96
|
-
return {
|
98
|
+
return { resolvedConnection: connections.find(({ clientID }) => clientID === idp_hint) };
|
97
99
|
}
|
98
100
|
else if (this.opts.idpDiscoveryPath) {
|
99
101
|
if (!isIdpFlow) {
|
100
102
|
// redirect to IdP selection page
|
101
|
-
const idpList =
|
102
|
-
|
103
|
-
|
104
|
-
|
103
|
+
const idpList = connections.map(({ idpMetadata, oidcProvider, clientID }) => {
|
104
|
+
var _a;
|
105
|
+
return JSON.stringify({
|
106
|
+
provider: (_a = idpMetadata === null || idpMetadata === void 0 ? void 0 : idpMetadata.provider) !== null && _a !== void 0 ? _a : oidcProvider === null || oidcProvider === void 0 ? void 0 : oidcProvider.provider,
|
107
|
+
clientID,
|
108
|
+
connectionIsSAML: idpMetadata && typeof idpMetadata === 'object',
|
109
|
+
connectionIsOIDC: oidcProvider && typeof oidcProvider === 'object',
|
110
|
+
});
|
111
|
+
});
|
105
112
|
return {
|
106
113
|
redirect_url: redirect.success(this.opts.externalUrl + this.opts.idpDiscoveryPath, Object.assign(Object.assign({}, originalParams), { idp: idpList })),
|
107
114
|
};
|
108
115
|
}
|
109
116
|
else {
|
110
|
-
|
117
|
+
// Relevant to IdP initiated SAML flow
|
118
|
+
const appList = connections.map(({ product, name, description, clientID }) => ({
|
111
119
|
product,
|
112
120
|
name,
|
113
121
|
description,
|
@@ -132,29 +140,31 @@ class OAuthController {
|
|
132
140
|
}
|
133
141
|
authorize(body) {
|
134
142
|
return __awaiter(this, void 0, void 0, function* () {
|
135
|
-
const { response_type = 'code', client_id, redirect_uri, state,
|
136
|
-
|
137
|
-
|
143
|
+
const { response_type = 'code', client_id, redirect_uri, state, scope, nonce, code_challenge, code_challenge_method = '', idp_hint, prompt, } = body;
|
144
|
+
const tenant = 'tenant' in body ? body.tenant : undefined;
|
145
|
+
const product = 'product' in body ? body.product : undefined;
|
146
|
+
const access_type = 'access_type' in body ? body.access_type : undefined;
|
147
|
+
const resource = 'resource' in body ? body.resource : undefined;
|
138
148
|
let requestedTenant = tenant;
|
139
149
|
let requestedProduct = product;
|
140
150
|
metrics.increment('oauthAuthorize');
|
141
151
|
if (!redirect_uri) {
|
142
152
|
throw new error_1.JacksonError('Please specify a redirect URL.', 400);
|
143
153
|
}
|
144
|
-
let
|
154
|
+
let connection;
|
145
155
|
const requestedScopes = getScopeValues(scope);
|
146
156
|
const requestedOIDCFlow = requestedScopes.includes('openid');
|
147
157
|
if (tenant && product) {
|
148
|
-
const
|
158
|
+
const connections = yield this.connectionStore.getByIndex({
|
149
159
|
name: utils_1.IndexNames.TenantProduct,
|
150
160
|
value: dbutils.keyFromParts(tenant, product),
|
151
161
|
});
|
152
|
-
if (!
|
153
|
-
throw new error_1.JacksonError('
|
162
|
+
if (!connections || connections.length === 0) {
|
163
|
+
throw new error_1.JacksonError('IdP connection not found.', 403);
|
154
164
|
}
|
155
|
-
|
165
|
+
connection = connections[0];
|
156
166
|
// Support multiple matches
|
157
|
-
const {
|
167
|
+
const { resolvedConnection, redirect_url } = this.resolveMultipleConnectionMatches(connections, idp_hint, {
|
158
168
|
response_type,
|
159
169
|
client_id,
|
160
170
|
redirect_uri,
|
@@ -167,17 +177,16 @@ class OAuthController {
|
|
167
177
|
nonce,
|
168
178
|
code_challenge,
|
169
179
|
code_challenge_method,
|
170
|
-
provider,
|
171
180
|
});
|
172
181
|
if (redirect_url) {
|
173
182
|
return { redirect_url };
|
174
183
|
}
|
175
|
-
if (
|
176
|
-
|
184
|
+
if (resolvedConnection) {
|
185
|
+
connection = resolvedConnection;
|
177
186
|
}
|
178
187
|
}
|
179
188
|
else if (client_id && client_id !== '' && client_id !== 'undefined' && client_id !== 'null') {
|
180
|
-
// if tenant and product are encoded in the client_id then we parse it and check for the relevant
|
189
|
+
// if tenant and product are encoded in the client_id then we parse it and check for the relevant connection(s)
|
181
190
|
let sp = getEncodedTenantProduct(client_id);
|
182
191
|
if (!sp && access_type) {
|
183
192
|
sp = getEncodedTenantProduct(access_type);
|
@@ -194,16 +203,16 @@ class OAuthController {
|
|
194
203
|
if (sp && sp.tenant && sp.product) {
|
195
204
|
requestedTenant = sp.tenant;
|
196
205
|
requestedProduct = sp.product;
|
197
|
-
const
|
206
|
+
const connections = yield this.connectionStore.getByIndex({
|
198
207
|
name: utils_1.IndexNames.TenantProduct,
|
199
208
|
value: dbutils.keyFromParts(sp.tenant, sp.product),
|
200
209
|
});
|
201
|
-
if (!
|
202
|
-
throw new error_1.JacksonError('
|
210
|
+
if (!connections || connections.length === 0) {
|
211
|
+
throw new error_1.JacksonError('IdP connection not found.', 403);
|
203
212
|
}
|
204
|
-
|
213
|
+
connection = connections[0];
|
205
214
|
// Support multiple matches
|
206
|
-
const {
|
215
|
+
const { resolvedConnection, redirect_url } = this.resolveMultipleConnectionMatches(connections, idp_hint, {
|
207
216
|
response_type,
|
208
217
|
client_id,
|
209
218
|
redirect_uri,
|
@@ -216,30 +225,29 @@ class OAuthController {
|
|
216
225
|
nonce,
|
217
226
|
code_challenge,
|
218
227
|
code_challenge_method,
|
219
|
-
provider,
|
220
228
|
});
|
221
229
|
if (redirect_url) {
|
222
230
|
return { redirect_url };
|
223
231
|
}
|
224
|
-
if (
|
225
|
-
|
232
|
+
if (resolvedConnection) {
|
233
|
+
connection = resolvedConnection;
|
226
234
|
}
|
227
235
|
}
|
228
236
|
else {
|
229
|
-
|
230
|
-
if (
|
231
|
-
requestedTenant =
|
232
|
-
requestedProduct =
|
237
|
+
connection = yield this.connectionStore.get(client_id);
|
238
|
+
if (connection) {
|
239
|
+
requestedTenant = connection.tenant;
|
240
|
+
requestedProduct = connection.product;
|
233
241
|
}
|
234
242
|
}
|
235
243
|
}
|
236
244
|
else {
|
237
245
|
throw new error_1.JacksonError('You need to specify client_id or tenant & product', 403);
|
238
246
|
}
|
239
|
-
if (!
|
240
|
-
throw new error_1.JacksonError('
|
247
|
+
if (!connection) {
|
248
|
+
throw new error_1.JacksonError('IdP connection not found.', 403);
|
241
249
|
}
|
242
|
-
if (!allowed.redirect(redirect_uri,
|
250
|
+
if (!allowed.redirect(redirect_uri, connection.redirectUrl)) {
|
243
251
|
throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
|
244
252
|
}
|
245
253
|
if (requestedOIDCFlow &&
|
@@ -271,37 +279,120 @@ class OAuthController {
|
|
271
279
|
}),
|
272
280
|
};
|
273
281
|
}
|
282
|
+
// Connection retrieved: Handover to IdP starts here
|
274
283
|
let ssoUrl;
|
275
284
|
let post = false;
|
276
|
-
const
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
+
const connectionIsSAML = connection.idpMetadata && typeof connection.idpMetadata === 'object';
|
286
|
+
const connectionIsOIDC = connection.oidcProvider && typeof connection.oidcProvider === 'object';
|
287
|
+
// Init sessionId
|
288
|
+
const sessionId = crypto_1.default.randomBytes(16).toString('hex');
|
289
|
+
const relayState = utils_1.relayStatePrefix + sessionId;
|
290
|
+
// SAML connection: SAML request will be constructed here
|
291
|
+
let samlReq;
|
292
|
+
if (connectionIsSAML) {
|
293
|
+
const { sso } = connection.idpMetadata;
|
294
|
+
if ('redirectUrl' in sso) {
|
295
|
+
// HTTP Redirect binding
|
296
|
+
ssoUrl = sso.redirectUrl;
|
297
|
+
}
|
298
|
+
else if ('postUrl' in sso) {
|
299
|
+
// HTTP-POST binding
|
300
|
+
ssoUrl = sso.postUrl;
|
301
|
+
post = true;
|
302
|
+
}
|
303
|
+
else {
|
304
|
+
return {
|
305
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
306
|
+
error: 'invalid_request',
|
307
|
+
error_description: 'SAML binding could not be retrieved',
|
308
|
+
redirect_uri,
|
309
|
+
state,
|
310
|
+
}),
|
311
|
+
};
|
312
|
+
}
|
313
|
+
try {
|
314
|
+
const { validTo } = new crypto_1.default.X509Certificate(connection.certs.publicKey);
|
315
|
+
const isValidExpiry = validTo != 'Bad time value' && new Date(validTo) > new Date();
|
316
|
+
if (!isValidExpiry) {
|
317
|
+
const certs = yield x509_1.default.generate();
|
318
|
+
connection.certs = certs;
|
319
|
+
if (certs) {
|
320
|
+
yield this.connectionStore.put(connection.clientID, connection, {
|
321
|
+
// secondary index on entityID
|
322
|
+
name: utils_1.IndexNames.EntityID,
|
323
|
+
value: connection.idpMetadata.entityID,
|
324
|
+
}, {
|
325
|
+
// secondary index on tenant + product
|
326
|
+
name: utils_1.IndexNames.TenantProduct,
|
327
|
+
value: dbutils.keyFromParts(connection.tenant, connection.product),
|
328
|
+
});
|
329
|
+
}
|
330
|
+
else {
|
331
|
+
throw new Error('Error generating x509 certs');
|
332
|
+
}
|
333
|
+
}
|
334
|
+
// We will get undefined or Space delimited, case sensitive list of ASCII string values in prompt
|
335
|
+
// If login is one of the value in prompt we want to enable forceAuthn
|
336
|
+
// Else use the saml connection forceAuthn value
|
337
|
+
const promptOptions = prompt ? prompt.split(' ').filter((p) => p === 'login') : [];
|
338
|
+
samlReq = saml20_1.default.request({
|
339
|
+
ssoUrl,
|
340
|
+
entityID: this.opts.samlAudience,
|
341
|
+
callbackUrl: this.opts.externalUrl + this.opts.samlPath,
|
342
|
+
signingKey: connection.certs.privateKey,
|
343
|
+
publicKey: connection.certs.publicKey,
|
344
|
+
forceAuthn: promptOptions.length > 0 ? true : !!connection.forceAuthn,
|
345
|
+
});
|
346
|
+
}
|
347
|
+
catch (err) {
|
348
|
+
return {
|
349
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
350
|
+
error: 'server_error',
|
351
|
+
error_description: (0, utils_1.getErrorMessage)(err),
|
352
|
+
redirect_uri,
|
353
|
+
state,
|
354
|
+
}),
|
355
|
+
};
|
356
|
+
}
|
285
357
|
}
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
358
|
+
// OIDC Connection: Issuer discovery, openid-client init and extraction of authorization endpoint happens here
|
359
|
+
let oidcCodeVerifier;
|
360
|
+
if (connectionIsOIDC) {
|
361
|
+
const { discoveryUrl, clientId, clientSecret } = connection.oidcProvider;
|
362
|
+
try {
|
363
|
+
const oidcIssuer = yield openid_client_1.Issuer.discover(discoveryUrl);
|
364
|
+
const oidcClient = new oidcIssuer.Client({
|
365
|
+
client_id: clientId,
|
366
|
+
client_secret: clientSecret,
|
367
|
+
redirect_uris: [this.opts.externalUrl + this.opts.oidcPath],
|
368
|
+
response_types: ['code'],
|
369
|
+
});
|
370
|
+
oidcCodeVerifier = openid_client_1.generators.codeVerifier();
|
371
|
+
const code_challenge = openid_client_1.generators.codeChallenge(oidcCodeVerifier);
|
372
|
+
ssoUrl = oidcClient.authorizationUrl({
|
373
|
+
scope: [...requestedScopes, 'openid', 'email', 'profile']
|
374
|
+
.filter((value, index, self) => self.indexOf(value) === index) // filter out duplicates
|
375
|
+
.join(' '),
|
376
|
+
code_challenge,
|
377
|
+
code_challenge_method: 'S256',
|
378
|
+
state: relayState,
|
379
|
+
});
|
380
|
+
}
|
381
|
+
catch (err) {
|
382
|
+
if (err) {
|
383
|
+
return {
|
384
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
385
|
+
error: 'server_error',
|
386
|
+
error_description: (err === null || err === void 0 ? void 0 : err.error) || (0, utils_1.getErrorMessage)(err),
|
387
|
+
redirect_uri,
|
388
|
+
state,
|
389
|
+
}),
|
390
|
+
};
|
391
|
+
}
|
392
|
+
}
|
295
393
|
}
|
394
|
+
// Session persistence happens here
|
296
395
|
try {
|
297
|
-
const samlReq = saml20_1.default.request({
|
298
|
-
ssoUrl,
|
299
|
-
entityID: this.opts.samlAudience,
|
300
|
-
callbackUrl: this.opts.externalUrl + this.opts.samlPath,
|
301
|
-
signingKey: samlConfig.certs.privateKey,
|
302
|
-
publicKey: samlConfig.certs.publicKey,
|
303
|
-
});
|
304
|
-
const sessionId = crypto_1.default.randomBytes(16).toString('hex');
|
305
396
|
const requested = { client_id, state, redirect_uri };
|
306
397
|
if (requestedTenant) {
|
307
398
|
requested.tenant = requestedTenant;
|
@@ -321,42 +412,58 @@ class OAuthController {
|
|
321
412
|
if (requestedScopes) {
|
322
413
|
requested.scope = requestedScopes;
|
323
414
|
}
|
324
|
-
|
325
|
-
id: samlReq.id,
|
415
|
+
const sessionObj = {
|
326
416
|
redirect_uri,
|
327
417
|
response_type,
|
328
418
|
state,
|
329
419
|
code_challenge,
|
330
420
|
code_challenge_method,
|
331
421
|
requested,
|
332
|
-
}
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
if (
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
422
|
+
};
|
423
|
+
yield this.sessionStore.put(sessionId, connectionIsSAML
|
424
|
+
? Object.assign(Object.assign({}, sessionObj), { id: samlReq === null || samlReq === void 0 ? void 0 : samlReq.id }) : Object.assign(Object.assign({}, sessionObj), { id: connection.clientID, oidcCodeVerifier }));
|
425
|
+
// Redirect to IdP
|
426
|
+
if (connectionIsSAML) {
|
427
|
+
let redirectUrl;
|
428
|
+
let authorizeForm;
|
429
|
+
if (!post) {
|
430
|
+
// HTTP Redirect binding
|
431
|
+
redirectUrl = redirect.success(ssoUrl, {
|
432
|
+
RelayState: relayState,
|
433
|
+
SAMLRequest: Buffer.from(yield deflateRawAsync(samlReq.request)).toString('base64'),
|
434
|
+
});
|
435
|
+
}
|
436
|
+
else {
|
437
|
+
// HTTP POST binding
|
438
|
+
authorizeForm = saml20_1.default.createPostForm(ssoUrl, [
|
439
|
+
{
|
440
|
+
name: 'RelayState',
|
441
|
+
value: relayState,
|
442
|
+
},
|
443
|
+
{
|
444
|
+
name: 'SAMLRequest',
|
445
|
+
value: Buffer.from(samlReq.request).toString('base64'),
|
446
|
+
},
|
447
|
+
]);
|
448
|
+
}
|
449
|
+
return {
|
450
|
+
redirect_url: redirectUrl,
|
451
|
+
authorize_form: authorizeForm,
|
452
|
+
};
|
453
|
+
}
|
454
|
+
else if (connectionIsOIDC) {
|
455
|
+
return { redirect_url: ssoUrl };
|
342
456
|
}
|
343
457
|
else {
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
value: Buffer.from(samlReq.request).toString('base64'),
|
353
|
-
},
|
354
|
-
]);
|
458
|
+
return {
|
459
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
460
|
+
error: 'invalid_request',
|
461
|
+
error_description: 'Connection appears to be misconfigured',
|
462
|
+
redirect_uri,
|
463
|
+
state,
|
464
|
+
}),
|
465
|
+
};
|
355
466
|
}
|
356
|
-
return {
|
357
|
-
redirect_url: redirectUrl,
|
358
|
-
authorize_form: authorizeForm,
|
359
|
-
};
|
360
467
|
}
|
361
468
|
catch (err) {
|
362
469
|
return {
|
@@ -386,22 +493,22 @@ class OAuthController {
|
|
386
493
|
if (!issuer) {
|
387
494
|
throw new error_1.JacksonError('Issuer not found.', 403);
|
388
495
|
}
|
389
|
-
const
|
496
|
+
const samlConnections = yield this.connectionStore.getByIndex({
|
390
497
|
name: utils_1.IndexNames.EntityID,
|
391
498
|
value: issuer,
|
392
499
|
});
|
393
|
-
if (!
|
394
|
-
throw new error_1.JacksonError('SAML
|
500
|
+
if (!samlConnections || samlConnections.length === 0) {
|
501
|
+
throw new error_1.JacksonError('SAML connection not found.', 403);
|
395
502
|
}
|
396
|
-
let
|
503
|
+
let samlConnection = samlConnections[0];
|
397
504
|
if (isIdPFlow) {
|
398
505
|
RelayState = '';
|
399
|
-
const {
|
506
|
+
const { resolvedConnection, app_select_form } = this.resolveMultipleConnectionMatches(samlConnections, idp_hint, { SAMLResponse }, true);
|
400
507
|
if (app_select_form) {
|
401
508
|
return { app_select_form };
|
402
509
|
}
|
403
|
-
if (
|
404
|
-
|
510
|
+
if (resolvedConnection) {
|
511
|
+
samlConnection = resolvedConnection;
|
405
512
|
}
|
406
513
|
}
|
407
514
|
let session;
|
@@ -413,33 +520,35 @@ class OAuthController {
|
|
413
520
|
}
|
414
521
|
if (!isIdPFlow) {
|
415
522
|
// Resolve if there are multiple matches for SP login. TODO: Support multiple matches for IdP login
|
416
|
-
|
417
|
-
|
418
|
-
?
|
419
|
-
:
|
523
|
+
samlConnection =
|
524
|
+
samlConnections.length === 1
|
525
|
+
? samlConnections[0]
|
526
|
+
: samlConnections.filter((c) => {
|
420
527
|
var _a, _b, _c;
|
421
528
|
return (c.clientID === ((_a = session === null || session === void 0 ? void 0 : session.requested) === null || _a === void 0 ? void 0 : _a.client_id) ||
|
422
529
|
(c.tenant === ((_b = session === null || session === void 0 ? void 0 : session.requested) === null || _b === void 0 ? void 0 : _b.tenant) && c.product === ((_c = session === null || session === void 0 ? void 0 : session.requested) === null || _c === void 0 ? void 0 : _c.product)));
|
423
530
|
})[0];
|
424
531
|
}
|
425
|
-
if (!
|
426
|
-
throw new error_1.JacksonError('SAML
|
532
|
+
if (!samlConnection) {
|
533
|
+
throw new error_1.JacksonError('SAML connection not found.', 403);
|
427
534
|
}
|
428
535
|
const validateOpts = {
|
429
|
-
thumbprint:
|
536
|
+
thumbprint: samlConnection.idpMetadata.thumbprint,
|
430
537
|
audience: this.opts.samlAudience,
|
431
|
-
privateKey:
|
538
|
+
privateKey: samlConnection.certs.privateKey,
|
432
539
|
};
|
433
|
-
if (session &&
|
540
|
+
if (session &&
|
541
|
+
session.redirect_uri &&
|
542
|
+
!allowed.redirect(session.redirect_uri, samlConnection.redirectUrl)) {
|
434
543
|
throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
|
435
544
|
}
|
436
545
|
if (session && session.id) {
|
437
546
|
validateOpts.inResponseTo = session.id;
|
438
547
|
}
|
439
548
|
let profile;
|
440
|
-
const redirect_uri = (session && session.redirect_uri) ||
|
549
|
+
const redirect_uri = (session && session.redirect_uri) || samlConnection.defaultRedirectUrl;
|
441
550
|
try {
|
442
|
-
profile = yield
|
551
|
+
profile = yield validateSAMLResponse(rawResponse, validateOpts);
|
443
552
|
}
|
444
553
|
catch (err) {
|
445
554
|
// return error to redirect_uri
|
@@ -456,8 +565,8 @@ class OAuthController {
|
|
456
565
|
const code = crypto_1.default.randomBytes(20).toString('hex');
|
457
566
|
const codeVal = {
|
458
567
|
profile,
|
459
|
-
clientID:
|
460
|
-
clientSecret:
|
568
|
+
clientID: samlConnection.clientID,
|
569
|
+
clientSecret: samlConnection.clientSecret,
|
461
570
|
requested: session === null || session === void 0 ? void 0 : session.requested,
|
462
571
|
};
|
463
572
|
if (session) {
|
@@ -494,6 +603,127 @@ class OAuthController {
|
|
494
603
|
return { redirect_url: redirectUrl };
|
495
604
|
});
|
496
605
|
}
|
606
|
+
extractOIDCUserProfile(tokenSet, oidcClient) {
|
607
|
+
var _a, _b, _c;
|
608
|
+
return __awaiter(this, void 0, void 0, function* () {
|
609
|
+
const profile = { claims: {} };
|
610
|
+
const idTokenClaims = tokenSet.claims();
|
611
|
+
const userinfo = yield oidcClient.userinfo(tokenSet);
|
612
|
+
profile.claims.id = idTokenClaims.sub;
|
613
|
+
profile.claims.email = (_a = idTokenClaims.email) !== null && _a !== void 0 ? _a : userinfo.email;
|
614
|
+
profile.claims.firstName = (_b = idTokenClaims.given_name) !== null && _b !== void 0 ? _b : userinfo.given_name;
|
615
|
+
profile.claims.lastName = (_c = idTokenClaims.family_name) !== null && _c !== void 0 ? _c : userinfo.family_name;
|
616
|
+
profile.claims.raw = userinfo;
|
617
|
+
return profile;
|
618
|
+
});
|
619
|
+
}
|
620
|
+
oidcAuthzResponse(body) {
|
621
|
+
return __awaiter(this, void 0, void 0, function* () {
|
622
|
+
const { code: opCode, state, error, error_description } = body;
|
623
|
+
let RelayState = state || '';
|
624
|
+
if (!RelayState) {
|
625
|
+
throw new error_1.JacksonError('State from original request is missing.', 403);
|
626
|
+
}
|
627
|
+
RelayState = RelayState.replace(utils_1.relayStatePrefix, '');
|
628
|
+
const session = yield this.sessionStore.get(RelayState);
|
629
|
+
if (!session) {
|
630
|
+
throw new error_1.JacksonError('Unable to validate state from the original request.', 403);
|
631
|
+
}
|
632
|
+
const oidcConnection = yield this.connectionStore.get(session.id);
|
633
|
+
if (session.redirect_uri && !allowed.redirect(session.redirect_uri, oidcConnection.redirectUrl)) {
|
634
|
+
throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
|
635
|
+
}
|
636
|
+
const redirect_uri = (session && session.redirect_uri) || oidcConnection.defaultRedirectUrl;
|
637
|
+
if (error) {
|
638
|
+
return {
|
639
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
640
|
+
error,
|
641
|
+
error_description: error_description !== null && error_description !== void 0 ? error_description : 'Authorization failure at OIDC Provider',
|
642
|
+
redirect_uri,
|
643
|
+
state: session.state,
|
644
|
+
}),
|
645
|
+
};
|
646
|
+
}
|
647
|
+
if (!opCode) {
|
648
|
+
return {
|
649
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
650
|
+
error: 'server_error',
|
651
|
+
error_description: 'Authorization code could not be retrieved from OIDC Provider',
|
652
|
+
redirect_uri,
|
653
|
+
state: session.state,
|
654
|
+
}),
|
655
|
+
};
|
656
|
+
}
|
657
|
+
// Reconstruct the oidcClient
|
658
|
+
const { discoveryUrl, clientId, clientSecret } = oidcConnection.oidcProvider;
|
659
|
+
let profile;
|
660
|
+
try {
|
661
|
+
const oidcIssuer = yield openid_client_1.Issuer.discover(discoveryUrl);
|
662
|
+
const oidcClient = new oidcIssuer.Client({
|
663
|
+
client_id: clientId,
|
664
|
+
client_secret: clientSecret,
|
665
|
+
redirect_uris: [this.opts.externalUrl + this.opts.oidcPath],
|
666
|
+
response_types: ['code'],
|
667
|
+
});
|
668
|
+
const tokenSet = yield oidcClient.callback(this.opts.externalUrl + this.opts.oidcPath, {
|
669
|
+
code: opCode,
|
670
|
+
}, { code_verifier: session.oidcCodeVerifier });
|
671
|
+
profile = yield this.extractOIDCUserProfile(tokenSet, oidcClient);
|
672
|
+
}
|
673
|
+
catch (err) {
|
674
|
+
if (err) {
|
675
|
+
return {
|
676
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
677
|
+
error: 'server_error',
|
678
|
+
error_description: (err === null || err === void 0 ? void 0 : err.error) || (0, utils_1.getErrorMessage)(err),
|
679
|
+
redirect_uri,
|
680
|
+
state: session.state,
|
681
|
+
}),
|
682
|
+
};
|
683
|
+
}
|
684
|
+
}
|
685
|
+
// store details against a code
|
686
|
+
const code = crypto_1.default.randomBytes(20).toString('hex');
|
687
|
+
const codeVal = {
|
688
|
+
profile,
|
689
|
+
clientID: oidcConnection.clientID,
|
690
|
+
clientSecret: oidcConnection.clientSecret,
|
691
|
+
requested: session === null || session === void 0 ? void 0 : session.requested,
|
692
|
+
};
|
693
|
+
if (session) {
|
694
|
+
codeVal.session = session;
|
695
|
+
}
|
696
|
+
try {
|
697
|
+
yield this.codeStore.put(code, codeVal);
|
698
|
+
}
|
699
|
+
catch (err) {
|
700
|
+
// return error to redirect_uri
|
701
|
+
return {
|
702
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
703
|
+
error: 'server_error',
|
704
|
+
error_description: (0, utils_1.getErrorMessage)(err),
|
705
|
+
redirect_uri,
|
706
|
+
state: session.state,
|
707
|
+
}),
|
708
|
+
};
|
709
|
+
}
|
710
|
+
const params = {
|
711
|
+
code,
|
712
|
+
};
|
713
|
+
if (session && session.state) {
|
714
|
+
params.state = session.state;
|
715
|
+
}
|
716
|
+
const redirectUrl = redirect.success(redirect_uri, params);
|
717
|
+
// delete the session
|
718
|
+
try {
|
719
|
+
yield this.sessionStore.delete(RelayState);
|
720
|
+
}
|
721
|
+
catch (_err) {
|
722
|
+
// ignore error
|
723
|
+
}
|
724
|
+
return { redirect_url: redirectUrl };
|
725
|
+
});
|
726
|
+
}
|
497
727
|
/**
|
498
728
|
* @swagger
|
499
729
|
*
|
@@ -515,13 +745,17 @@ class OAuthController {
|
|
515
745
|
* - name: client_id
|
516
746
|
* in: formData
|
517
747
|
* type: string
|
518
|
-
* description: Use the client_id returned by the SAML
|
748
|
+
* description: Use the client_id returned by the SAML connection API
|
519
749
|
* required: true
|
520
750
|
* - name: client_secret
|
521
751
|
* in: formData
|
522
752
|
* type: string
|
523
|
-
* description: Use the client_secret returned by the SAML
|
753
|
+
* description: Use the client_secret returned by the SAML connection API
|
524
754
|
* required: true
|
755
|
+
* - name: code_verifier
|
756
|
+
* in: formData
|
757
|
+
* type: string
|
758
|
+
* description: code_verifier against the code_challenge in the authz request (relevant to PKCE flow)
|
525
759
|
* - name: redirect_uri
|
526
760
|
* in: formData
|
527
761
|
* type: string
|
@@ -550,9 +784,12 @@ class OAuthController {
|
|
550
784
|
* expires_in: 300
|
551
785
|
*/
|
552
786
|
token(body) {
|
553
|
-
var _a, _b, _c;
|
787
|
+
var _a, _b, _c, _d, _e;
|
554
788
|
return __awaiter(this, void 0, void 0, function* () {
|
555
|
-
const {
|
789
|
+
const { code, grant_type = 'authorization_code', redirect_uri } = body;
|
790
|
+
const client_id = 'client_id' in body ? body.client_id : undefined;
|
791
|
+
const client_secret = 'client_secret' in body ? body.client_secret : undefined;
|
792
|
+
const code_verifier = 'code_verifier' in body ? body.code_verifier : undefined;
|
556
793
|
metrics.increment('oauthToken');
|
557
794
|
if (grant_type !== 'authorization_code') {
|
558
795
|
throw new error_1.JacksonError('Unsupported grant_type', 400);
|
@@ -590,6 +827,9 @@ class OAuthController {
|
|
590
827
|
}
|
591
828
|
}
|
592
829
|
else {
|
830
|
+
if (sp.tenant !== ((_b = codeVal.requested) === null || _b === void 0 ? void 0 : _b.tenant) || sp.product !== ((_c = codeVal.requested) === null || _c === void 0 ? void 0 : _c.product)) {
|
831
|
+
throw new error_1.JacksonError('Invalid tenant or product', 401);
|
832
|
+
}
|
593
833
|
// encoded client_id, verify client_secret
|
594
834
|
if (client_secret !== this.opts.clientSecretVerifier) {
|
595
835
|
throw new error_1.JacksonError('Invalid client_secret', 401);
|
@@ -608,8 +848,8 @@ class OAuthController {
|
|
608
848
|
// store details against a token
|
609
849
|
const token = crypto_1.default.randomBytes(20).toString('hex');
|
610
850
|
const tokenVal = Object.assign(Object.assign({}, codeVal.profile), { requested: codeVal.requested });
|
611
|
-
const requestedOIDCFlow = !!((
|
612
|
-
const requestHasNonce = !!((
|
851
|
+
const requestedOIDCFlow = !!((_d = codeVal.requested) === null || _d === void 0 ? void 0 : _d.oidc);
|
852
|
+
const requestHasNonce = !!((_e = codeVal.requested) === null || _e === void 0 ? void 0 : _e.nonce);
|
613
853
|
if (requestedOIDCFlow) {
|
614
854
|
const { jwtSigningKeys, jwsAlg } = this.opts.openid;
|
615
855
|
if (!jwtSigningKeys || !(0, utils_1.isJWSKeyPairLoaded)(jwtSigningKeys)) {
|
@@ -671,11 +911,21 @@ class OAuthController {
|
|
671
911
|
* type: string
|
672
912
|
* lastName:
|
673
913
|
* type: string
|
914
|
+
* raw:
|
915
|
+
* type: object
|
916
|
+
* requested:
|
917
|
+
* type: object
|
674
918
|
* example:
|
675
919
|
* id: 32b5af58fdf
|
676
920
|
* email: jackson@coolstartup.com
|
677
921
|
* firstName: SAML
|
678
922
|
* lastName: Jackson
|
923
|
+
* raw: {
|
924
|
+
*
|
925
|
+
* }
|
926
|
+
* requested: {
|
927
|
+
*
|
928
|
+
* }
|
679
929
|
*/
|
680
930
|
userInfo(token) {
|
681
931
|
return __awaiter(this, void 0, void 0, function* () {
|