@boxyhq/saml-jackson 1.2.2 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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* () {
|