@boxyhq/saml-jackson 1.2.2 → 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 -378
- 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 +361 -140
- package/dist/controller/utils.d.ts +10 -2
- package/dist/controller/utils.js +88 -1
- 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/typings.d.ts +109 -35
- package/package.json +3 -2
- 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"));
|
@@ -51,7 +52,7 @@ const redirect = __importStar(require("./oauth/redirect"));
|
|
51
52
|
const utils_1 = require("./utils");
|
52
53
|
const x509_1 = __importDefault(require("../saml/x509"));
|
53
54
|
const deflateRawAsync = (0, util_1.promisify)(zlib_1.deflateRaw);
|
54
|
-
const
|
55
|
+
const validateSAMLResponse = (rawResponse, validateOpts) => __awaiter(void 0, void 0, void 0, function* () {
|
55
56
|
const profile = yield saml20_1.default.validate(rawResponse, validateOpts);
|
56
57
|
if (profile && profile.claims) {
|
57
58
|
// we map claims to our attributes id, email, firstName, lastName where possible. We also map original claims to raw
|
@@ -84,31 +85,37 @@ function getScopeValues(scope) {
|
|
84
85
|
return typeof scope === 'string' ? scope.split(' ').filter((s) => s.length > 0) : [];
|
85
86
|
}
|
86
87
|
class OAuthController {
|
87
|
-
constructor({
|
88
|
-
this.
|
88
|
+
constructor({ connectionStore, sessionStore, codeStore, tokenStore, opts }) {
|
89
|
+
this.connectionStore = connectionStore;
|
89
90
|
this.sessionStore = sessionStore;
|
90
91
|
this.codeStore = codeStore;
|
91
92
|
this.tokenStore = tokenStore;
|
92
93
|
this.opts = opts;
|
93
94
|
}
|
94
|
-
|
95
|
-
if (
|
95
|
+
resolveMultipleConnectionMatches(connections, idp_hint, originalParams, isIdpFlow = false) {
|
96
|
+
if (connections.length > 1) {
|
96
97
|
if (idp_hint) {
|
97
|
-
return {
|
98
|
+
return { resolvedConnection: connections.find(({ clientID }) => clientID === idp_hint) };
|
98
99
|
}
|
99
100
|
else if (this.opts.idpDiscoveryPath) {
|
100
101
|
if (!isIdpFlow) {
|
101
102
|
// redirect to IdP selection page
|
102
|
-
const idpList =
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
+
});
|
106
112
|
return {
|
107
113
|
redirect_url: redirect.success(this.opts.externalUrl + this.opts.idpDiscoveryPath, Object.assign(Object.assign({}, originalParams), { idp: idpList })),
|
108
114
|
};
|
109
115
|
}
|
110
116
|
else {
|
111
|
-
|
117
|
+
// Relevant to IdP initiated SAML flow
|
118
|
+
const appList = connections.map(({ product, name, description, clientID }) => ({
|
112
119
|
product,
|
113
120
|
name,
|
114
121
|
description,
|
@@ -133,29 +140,31 @@ class OAuthController {
|
|
133
140
|
}
|
134
141
|
authorize(body) {
|
135
142
|
return __awaiter(this, void 0, void 0, function* () {
|
136
|
-
const { response_type = 'code', client_id, redirect_uri, state,
|
137
|
-
|
138
|
-
|
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;
|
139
148
|
let requestedTenant = tenant;
|
140
149
|
let requestedProduct = product;
|
141
150
|
metrics.increment('oauthAuthorize');
|
142
151
|
if (!redirect_uri) {
|
143
152
|
throw new error_1.JacksonError('Please specify a redirect URL.', 400);
|
144
153
|
}
|
145
|
-
let
|
154
|
+
let connection;
|
146
155
|
const requestedScopes = getScopeValues(scope);
|
147
156
|
const requestedOIDCFlow = requestedScopes.includes('openid');
|
148
157
|
if (tenant && product) {
|
149
|
-
const
|
158
|
+
const connections = yield this.connectionStore.getByIndex({
|
150
159
|
name: utils_1.IndexNames.TenantProduct,
|
151
160
|
value: dbutils.keyFromParts(tenant, product),
|
152
161
|
});
|
153
|
-
if (!
|
154
|
-
throw new error_1.JacksonError('
|
162
|
+
if (!connections || connections.length === 0) {
|
163
|
+
throw new error_1.JacksonError('IdP connection not found.', 403);
|
155
164
|
}
|
156
|
-
|
165
|
+
connection = connections[0];
|
157
166
|
// Support multiple matches
|
158
|
-
const {
|
167
|
+
const { resolvedConnection, redirect_url } = this.resolveMultipleConnectionMatches(connections, idp_hint, {
|
159
168
|
response_type,
|
160
169
|
client_id,
|
161
170
|
redirect_uri,
|
@@ -168,17 +177,16 @@ class OAuthController {
|
|
168
177
|
nonce,
|
169
178
|
code_challenge,
|
170
179
|
code_challenge_method,
|
171
|
-
provider,
|
172
180
|
});
|
173
181
|
if (redirect_url) {
|
174
182
|
return { redirect_url };
|
175
183
|
}
|
176
|
-
if (
|
177
|
-
|
184
|
+
if (resolvedConnection) {
|
185
|
+
connection = resolvedConnection;
|
178
186
|
}
|
179
187
|
}
|
180
188
|
else if (client_id && client_id !== '' && client_id !== 'undefined' && client_id !== 'null') {
|
181
|
-
// 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)
|
182
190
|
let sp = getEncodedTenantProduct(client_id);
|
183
191
|
if (!sp && access_type) {
|
184
192
|
sp = getEncodedTenantProduct(access_type);
|
@@ -195,16 +203,16 @@ class OAuthController {
|
|
195
203
|
if (sp && sp.tenant && sp.product) {
|
196
204
|
requestedTenant = sp.tenant;
|
197
205
|
requestedProduct = sp.product;
|
198
|
-
const
|
206
|
+
const connections = yield this.connectionStore.getByIndex({
|
199
207
|
name: utils_1.IndexNames.TenantProduct,
|
200
208
|
value: dbutils.keyFromParts(sp.tenant, sp.product),
|
201
209
|
});
|
202
|
-
if (!
|
203
|
-
throw new error_1.JacksonError('
|
210
|
+
if (!connections || connections.length === 0) {
|
211
|
+
throw new error_1.JacksonError('IdP connection not found.', 403);
|
204
212
|
}
|
205
|
-
|
213
|
+
connection = connections[0];
|
206
214
|
// Support multiple matches
|
207
|
-
const {
|
215
|
+
const { resolvedConnection, redirect_url } = this.resolveMultipleConnectionMatches(connections, idp_hint, {
|
208
216
|
response_type,
|
209
217
|
client_id,
|
210
218
|
redirect_uri,
|
@@ -217,30 +225,29 @@ class OAuthController {
|
|
217
225
|
nonce,
|
218
226
|
code_challenge,
|
219
227
|
code_challenge_method,
|
220
|
-
provider,
|
221
228
|
});
|
222
229
|
if (redirect_url) {
|
223
230
|
return { redirect_url };
|
224
231
|
}
|
225
|
-
if (
|
226
|
-
|
232
|
+
if (resolvedConnection) {
|
233
|
+
connection = resolvedConnection;
|
227
234
|
}
|
228
235
|
}
|
229
236
|
else {
|
230
|
-
|
231
|
-
if (
|
232
|
-
requestedTenant =
|
233
|
-
requestedProduct =
|
237
|
+
connection = yield this.connectionStore.get(client_id);
|
238
|
+
if (connection) {
|
239
|
+
requestedTenant = connection.tenant;
|
240
|
+
requestedProduct = connection.product;
|
234
241
|
}
|
235
242
|
}
|
236
243
|
}
|
237
244
|
else {
|
238
245
|
throw new error_1.JacksonError('You need to specify client_id or tenant & product', 403);
|
239
246
|
}
|
240
|
-
if (!
|
241
|
-
throw new error_1.JacksonError('
|
247
|
+
if (!connection) {
|
248
|
+
throw new error_1.JacksonError('IdP connection not found.', 403);
|
242
249
|
}
|
243
|
-
if (!allowed.redirect(redirect_uri,
|
250
|
+
if (!allowed.redirect(redirect_uri, connection.redirectUrl)) {
|
244
251
|
throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
|
245
252
|
}
|
246
253
|
if (requestedOIDCFlow &&
|
@@ -272,62 +279,120 @@ class OAuthController {
|
|
272
279
|
}),
|
273
280
|
};
|
274
281
|
}
|
282
|
+
// Connection retrieved: Handover to IdP starts here
|
275
283
|
let ssoUrl;
|
276
284
|
let post = false;
|
277
|
-
const
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
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
|
+
}
|
313
333
|
}
|
314
|
-
|
315
|
-
|
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
|
+
}
|
357
|
+
}
|
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
|
+
};
|
316
391
|
}
|
317
392
|
}
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
const promptOptions = prompt ? prompt.split(' ').filter((p) => p === 'login') : [];
|
322
|
-
const samlReq = saml20_1.default.request({
|
323
|
-
ssoUrl,
|
324
|
-
entityID: this.opts.samlAudience,
|
325
|
-
callbackUrl: this.opts.externalUrl + this.opts.samlPath,
|
326
|
-
signingKey: samlConfig.certs.privateKey,
|
327
|
-
publicKey: samlConfig.certs.publicKey,
|
328
|
-
forceAuthn: promptOptions.length > 0 ? true : !!samlConfig.forceAuthn,
|
329
|
-
});
|
330
|
-
const sessionId = crypto_1.default.randomBytes(16).toString('hex');
|
393
|
+
}
|
394
|
+
// Session persistence happens here
|
395
|
+
try {
|
331
396
|
const requested = { client_id, state, redirect_uri };
|
332
397
|
if (requestedTenant) {
|
333
398
|
requested.tenant = requestedTenant;
|
@@ -347,42 +412,58 @@ class OAuthController {
|
|
347
412
|
if (requestedScopes) {
|
348
413
|
requested.scope = requestedScopes;
|
349
414
|
}
|
350
|
-
|
351
|
-
id: samlReq.id,
|
415
|
+
const sessionObj = {
|
352
416
|
redirect_uri,
|
353
417
|
response_type,
|
354
418
|
state,
|
355
419
|
code_challenge,
|
356
420
|
code_challenge_method,
|
357
421
|
requested,
|
358
|
-
}
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
if (
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
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 };
|
368
456
|
}
|
369
457
|
else {
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
value: Buffer.from(samlReq.request).toString('base64'),
|
379
|
-
},
|
380
|
-
]);
|
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
|
+
};
|
381
466
|
}
|
382
|
-
return {
|
383
|
-
redirect_url: redirectUrl,
|
384
|
-
authorize_form: authorizeForm,
|
385
|
-
};
|
386
467
|
}
|
387
468
|
catch (err) {
|
388
469
|
return {
|
@@ -412,22 +493,22 @@ class OAuthController {
|
|
412
493
|
if (!issuer) {
|
413
494
|
throw new error_1.JacksonError('Issuer not found.', 403);
|
414
495
|
}
|
415
|
-
const
|
496
|
+
const samlConnections = yield this.connectionStore.getByIndex({
|
416
497
|
name: utils_1.IndexNames.EntityID,
|
417
498
|
value: issuer,
|
418
499
|
});
|
419
|
-
if (!
|
420
|
-
throw new error_1.JacksonError('SAML
|
500
|
+
if (!samlConnections || samlConnections.length === 0) {
|
501
|
+
throw new error_1.JacksonError('SAML connection not found.', 403);
|
421
502
|
}
|
422
|
-
let
|
503
|
+
let samlConnection = samlConnections[0];
|
423
504
|
if (isIdPFlow) {
|
424
505
|
RelayState = '';
|
425
|
-
const {
|
506
|
+
const { resolvedConnection, app_select_form } = this.resolveMultipleConnectionMatches(samlConnections, idp_hint, { SAMLResponse }, true);
|
426
507
|
if (app_select_form) {
|
427
508
|
return { app_select_form };
|
428
509
|
}
|
429
|
-
if (
|
430
|
-
|
510
|
+
if (resolvedConnection) {
|
511
|
+
samlConnection = resolvedConnection;
|
431
512
|
}
|
432
513
|
}
|
433
514
|
let session;
|
@@ -439,33 +520,35 @@ class OAuthController {
|
|
439
520
|
}
|
440
521
|
if (!isIdPFlow) {
|
441
522
|
// Resolve if there are multiple matches for SP login. TODO: Support multiple matches for IdP login
|
442
|
-
|
443
|
-
|
444
|
-
?
|
445
|
-
:
|
523
|
+
samlConnection =
|
524
|
+
samlConnections.length === 1
|
525
|
+
? samlConnections[0]
|
526
|
+
: samlConnections.filter((c) => {
|
446
527
|
var _a, _b, _c;
|
447
528
|
return (c.clientID === ((_a = session === null || session === void 0 ? void 0 : session.requested) === null || _a === void 0 ? void 0 : _a.client_id) ||
|
448
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)));
|
449
530
|
})[0];
|
450
531
|
}
|
451
|
-
if (!
|
452
|
-
throw new error_1.JacksonError('SAML
|
532
|
+
if (!samlConnection) {
|
533
|
+
throw new error_1.JacksonError('SAML connection not found.', 403);
|
453
534
|
}
|
454
535
|
const validateOpts = {
|
455
|
-
thumbprint:
|
536
|
+
thumbprint: samlConnection.idpMetadata.thumbprint,
|
456
537
|
audience: this.opts.samlAudience,
|
457
|
-
privateKey:
|
538
|
+
privateKey: samlConnection.certs.privateKey,
|
458
539
|
};
|
459
|
-
if (session &&
|
540
|
+
if (session &&
|
541
|
+
session.redirect_uri &&
|
542
|
+
!allowed.redirect(session.redirect_uri, samlConnection.redirectUrl)) {
|
460
543
|
throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
|
461
544
|
}
|
462
545
|
if (session && session.id) {
|
463
546
|
validateOpts.inResponseTo = session.id;
|
464
547
|
}
|
465
548
|
let profile;
|
466
|
-
const redirect_uri = (session && session.redirect_uri) ||
|
549
|
+
const redirect_uri = (session && session.redirect_uri) || samlConnection.defaultRedirectUrl;
|
467
550
|
try {
|
468
|
-
profile = yield
|
551
|
+
profile = yield validateSAMLResponse(rawResponse, validateOpts);
|
469
552
|
}
|
470
553
|
catch (err) {
|
471
554
|
// return error to redirect_uri
|
@@ -482,8 +565,8 @@ class OAuthController {
|
|
482
565
|
const code = crypto_1.default.randomBytes(20).toString('hex');
|
483
566
|
const codeVal = {
|
484
567
|
profile,
|
485
|
-
clientID:
|
486
|
-
clientSecret:
|
568
|
+
clientID: samlConnection.clientID,
|
569
|
+
clientSecret: samlConnection.clientSecret,
|
487
570
|
requested: session === null || session === void 0 ? void 0 : session.requested,
|
488
571
|
};
|
489
572
|
if (session) {
|
@@ -520,6 +603,127 @@ class OAuthController {
|
|
520
603
|
return { redirect_url: redirectUrl };
|
521
604
|
});
|
522
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
|
+
}
|
523
727
|
/**
|
524
728
|
* @swagger
|
525
729
|
*
|
@@ -541,13 +745,17 @@ class OAuthController {
|
|
541
745
|
* - name: client_id
|
542
746
|
* in: formData
|
543
747
|
* type: string
|
544
|
-
* description: Use the client_id returned by the SAML
|
748
|
+
* description: Use the client_id returned by the SAML connection API
|
545
749
|
* required: true
|
546
750
|
* - name: client_secret
|
547
751
|
* in: formData
|
548
752
|
* type: string
|
549
|
-
* description: Use the client_secret returned by the SAML
|
753
|
+
* description: Use the client_secret returned by the SAML connection API
|
550
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)
|
551
759
|
* - name: redirect_uri
|
552
760
|
* in: formData
|
553
761
|
* type: string
|
@@ -578,7 +786,10 @@ class OAuthController {
|
|
578
786
|
token(body) {
|
579
787
|
var _a, _b, _c, _d, _e;
|
580
788
|
return __awaiter(this, void 0, void 0, function* () {
|
581
|
-
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;
|
582
793
|
metrics.increment('oauthToken');
|
583
794
|
if (grant_type !== 'authorization_code') {
|
584
795
|
throw new error_1.JacksonError('Unsupported grant_type', 400);
|
@@ -700,11 +911,21 @@ class OAuthController {
|
|
700
911
|
* type: string
|
701
912
|
* lastName:
|
702
913
|
* type: string
|
914
|
+
* raw:
|
915
|
+
* type: object
|
916
|
+
* requested:
|
917
|
+
* type: object
|
703
918
|
* example:
|
704
919
|
* id: 32b5af58fdf
|
705
920
|
* email: jackson@coolstartup.com
|
706
921
|
* firstName: SAML
|
707
922
|
* lastName: Jackson
|
923
|
+
* raw: {
|
924
|
+
*
|
925
|
+
* }
|
926
|
+
* requested: {
|
927
|
+
*
|
928
|
+
* }
|
708
929
|
*/
|
709
930
|
userInfo(token) {
|
710
931
|
return __awaiter(this, void 0, void 0, function* () {
|