@boxyhq/saml-jackson 1.2.2 → 1.3.1
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 +460 -204
- package/dist/controller/api.js +561 -377
- package/dist/controller/connection/oidc.d.ts +9 -0
- package/dist/controller/connection/oidc.js +145 -0
- package/dist/controller/connection/saml.d.ts +9 -0
- package/dist/controller/connection/saml.js +174 -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 +375 -143
- package/dist/controller/oidc-discovery.js +2 -1
- 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 +23 -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 +155 -36
- package/package.json +11 -10
- 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,
|
@@ -132,30 +139,33 @@ class OAuthController {
|
|
132
139
|
return {};
|
133
140
|
}
|
134
141
|
authorize(body) {
|
142
|
+
var _a;
|
135
143
|
return __awaiter(this, void 0, void 0, function* () {
|
136
|
-
const { response_type = 'code', client_id, redirect_uri, state,
|
137
|
-
|
138
|
-
|
144
|
+
const { response_type = 'code', client_id, redirect_uri, state, scope, nonce, code_challenge, code_challenge_method = '', idp_hint, prompt, } = body;
|
145
|
+
const tenant = 'tenant' in body ? body.tenant : undefined;
|
146
|
+
const product = 'product' in body ? body.product : undefined;
|
147
|
+
const access_type = 'access_type' in body ? body.access_type : undefined;
|
148
|
+
const resource = 'resource' in body ? body.resource : undefined;
|
139
149
|
let requestedTenant = tenant;
|
140
150
|
let requestedProduct = product;
|
141
151
|
metrics.increment('oauthAuthorize');
|
142
152
|
if (!redirect_uri) {
|
143
153
|
throw new error_1.JacksonError('Please specify a redirect URL.', 400);
|
144
154
|
}
|
145
|
-
let
|
155
|
+
let connection;
|
146
156
|
const requestedScopes = getScopeValues(scope);
|
147
157
|
const requestedOIDCFlow = requestedScopes.includes('openid');
|
148
158
|
if (tenant && product) {
|
149
|
-
const
|
159
|
+
const connections = yield this.connectionStore.getByIndex({
|
150
160
|
name: utils_1.IndexNames.TenantProduct,
|
151
161
|
value: dbutils.keyFromParts(tenant, product),
|
152
162
|
});
|
153
|
-
if (!
|
154
|
-
throw new error_1.JacksonError('
|
163
|
+
if (!connections || connections.length === 0) {
|
164
|
+
throw new error_1.JacksonError('IdP connection not found.', 403);
|
155
165
|
}
|
156
|
-
|
166
|
+
connection = connections[0];
|
157
167
|
// Support multiple matches
|
158
|
-
const {
|
168
|
+
const { resolvedConnection, redirect_url } = this.resolveMultipleConnectionMatches(connections, idp_hint, {
|
159
169
|
response_type,
|
160
170
|
client_id,
|
161
171
|
redirect_uri,
|
@@ -168,17 +178,16 @@ class OAuthController {
|
|
168
178
|
nonce,
|
169
179
|
code_challenge,
|
170
180
|
code_challenge_method,
|
171
|
-
provider,
|
172
181
|
});
|
173
182
|
if (redirect_url) {
|
174
183
|
return { redirect_url };
|
175
184
|
}
|
176
|
-
if (
|
177
|
-
|
185
|
+
if (resolvedConnection) {
|
186
|
+
connection = resolvedConnection;
|
178
187
|
}
|
179
188
|
}
|
180
189
|
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
|
190
|
+
// if tenant and product are encoded in the client_id then we parse it and check for the relevant connection(s)
|
182
191
|
let sp = getEncodedTenantProduct(client_id);
|
183
192
|
if (!sp && access_type) {
|
184
193
|
sp = getEncodedTenantProduct(access_type);
|
@@ -195,16 +204,16 @@ class OAuthController {
|
|
195
204
|
if (sp && sp.tenant && sp.product) {
|
196
205
|
requestedTenant = sp.tenant;
|
197
206
|
requestedProduct = sp.product;
|
198
|
-
const
|
207
|
+
const connections = yield this.connectionStore.getByIndex({
|
199
208
|
name: utils_1.IndexNames.TenantProduct,
|
200
209
|
value: dbutils.keyFromParts(sp.tenant, sp.product),
|
201
210
|
});
|
202
|
-
if (!
|
203
|
-
throw new error_1.JacksonError('
|
211
|
+
if (!connections || connections.length === 0) {
|
212
|
+
throw new error_1.JacksonError('IdP connection not found.', 403);
|
204
213
|
}
|
205
|
-
|
214
|
+
connection = connections[0];
|
206
215
|
// Support multiple matches
|
207
|
-
const {
|
216
|
+
const { resolvedConnection, redirect_url } = this.resolveMultipleConnectionMatches(connections, idp_hint, {
|
208
217
|
response_type,
|
209
218
|
client_id,
|
210
219
|
redirect_uri,
|
@@ -217,34 +226,33 @@ class OAuthController {
|
|
217
226
|
nonce,
|
218
227
|
code_challenge,
|
219
228
|
code_challenge_method,
|
220
|
-
provider,
|
221
229
|
});
|
222
230
|
if (redirect_url) {
|
223
231
|
return { redirect_url };
|
224
232
|
}
|
225
|
-
if (
|
226
|
-
|
233
|
+
if (resolvedConnection) {
|
234
|
+
connection = resolvedConnection;
|
227
235
|
}
|
228
236
|
}
|
229
237
|
else {
|
230
|
-
|
231
|
-
if (
|
232
|
-
requestedTenant =
|
233
|
-
requestedProduct =
|
238
|
+
connection = yield this.connectionStore.get(client_id);
|
239
|
+
if (connection) {
|
240
|
+
requestedTenant = connection.tenant;
|
241
|
+
requestedProduct = connection.product;
|
234
242
|
}
|
235
243
|
}
|
236
244
|
}
|
237
245
|
else {
|
238
246
|
throw new error_1.JacksonError('You need to specify client_id or tenant & product', 403);
|
239
247
|
}
|
240
|
-
if (!
|
241
|
-
throw new error_1.JacksonError('
|
248
|
+
if (!connection) {
|
249
|
+
throw new error_1.JacksonError('IdP connection not found.', 403);
|
242
250
|
}
|
243
|
-
if (!allowed.redirect(redirect_uri,
|
251
|
+
if (!allowed.redirect(redirect_uri, connection.redirectUrl)) {
|
244
252
|
throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
|
245
253
|
}
|
246
254
|
if (requestedOIDCFlow &&
|
247
|
-
(!this.opts.openid.jwtSigningKeys || !(0, utils_1.isJWSKeyPairLoaded)(this.opts.openid.jwtSigningKeys))) {
|
255
|
+
(!((_a = this.opts.openid) === null || _a === void 0 ? void 0 : _a.jwtSigningKeys) || !(0, utils_1.isJWSKeyPairLoaded)(this.opts.openid.jwtSigningKeys))) {
|
248
256
|
return {
|
249
257
|
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
250
258
|
error: 'server_error',
|
@@ -272,62 +280,130 @@ class OAuthController {
|
|
272
280
|
}),
|
273
281
|
};
|
274
282
|
}
|
283
|
+
// Connection retrieved: Handover to IdP starts here
|
275
284
|
let ssoUrl;
|
276
285
|
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
|
-
|
286
|
+
const connectionIsSAML = connection.idpMetadata && typeof connection.idpMetadata === 'object';
|
287
|
+
const connectionIsOIDC = connection.oidcProvider && typeof connection.oidcProvider === 'object';
|
288
|
+
// Init sessionId
|
289
|
+
const sessionId = crypto_1.default.randomBytes(16).toString('hex');
|
290
|
+
const relayState = utils_1.relayStatePrefix + sessionId;
|
291
|
+
// SAML connection: SAML request will be constructed here
|
292
|
+
let samlReq;
|
293
|
+
if (connectionIsSAML) {
|
294
|
+
const { sso } = connection.idpMetadata;
|
295
|
+
if ('redirectUrl' in sso) {
|
296
|
+
// HTTP Redirect binding
|
297
|
+
ssoUrl = sso.redirectUrl;
|
298
|
+
}
|
299
|
+
else if ('postUrl' in sso) {
|
300
|
+
// HTTP-POST binding
|
301
|
+
ssoUrl = sso.postUrl;
|
302
|
+
post = true;
|
303
|
+
}
|
304
|
+
else {
|
305
|
+
return {
|
306
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
307
|
+
error: 'invalid_request',
|
308
|
+
error_description: 'SAML binding could not be retrieved',
|
309
|
+
redirect_uri,
|
310
|
+
state,
|
311
|
+
}),
|
312
|
+
};
|
313
|
+
}
|
314
|
+
try {
|
315
|
+
const { validTo } = new crypto_1.default.X509Certificate(connection.certs.publicKey);
|
316
|
+
const isValidExpiry = validTo != 'Bad time value' && new Date(validTo) > new Date();
|
317
|
+
if (!isValidExpiry) {
|
318
|
+
const certs = yield x509_1.default.generate();
|
319
|
+
connection.certs = certs;
|
320
|
+
if (certs) {
|
321
|
+
yield this.connectionStore.put(connection.clientID, connection, {
|
322
|
+
// secondary index on entityID
|
323
|
+
name: utils_1.IndexNames.EntityID,
|
324
|
+
value: connection.idpMetadata.entityID,
|
325
|
+
}, {
|
326
|
+
// secondary index on tenant + product
|
327
|
+
name: utils_1.IndexNames.TenantProduct,
|
328
|
+
value: dbutils.keyFromParts(connection.tenant, connection.product),
|
329
|
+
});
|
330
|
+
}
|
331
|
+
else {
|
332
|
+
throw new Error('Error generating x509 certs');
|
333
|
+
}
|
313
334
|
}
|
314
|
-
|
315
|
-
|
335
|
+
// We will get undefined or Space delimited, case sensitive list of ASCII string values in prompt
|
336
|
+
// If login is one of the value in prompt we want to enable forceAuthn
|
337
|
+
// Else use the saml connection forceAuthn value
|
338
|
+
const promptOptions = prompt ? prompt.split(' ').filter((p) => p === 'login') : [];
|
339
|
+
samlReq = saml20_1.default.request({
|
340
|
+
ssoUrl,
|
341
|
+
entityID: this.opts.samlAudience,
|
342
|
+
callbackUrl: this.opts.externalUrl + this.opts.samlPath,
|
343
|
+
signingKey: connection.certs.privateKey,
|
344
|
+
publicKey: connection.certs.publicKey,
|
345
|
+
forceAuthn: promptOptions.length > 0 ? true : !!connection.forceAuthn,
|
346
|
+
});
|
347
|
+
}
|
348
|
+
catch (err) {
|
349
|
+
return {
|
350
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
351
|
+
error: 'server_error',
|
352
|
+
error_description: (0, utils_1.getErrorMessage)(err),
|
353
|
+
redirect_uri,
|
354
|
+
state,
|
355
|
+
}),
|
356
|
+
};
|
357
|
+
}
|
358
|
+
}
|
359
|
+
// OIDC Connection: Issuer discovery, openid-client init and extraction of authorization endpoint happens here
|
360
|
+
let oidcCodeVerifier;
|
361
|
+
if (connectionIsOIDC) {
|
362
|
+
if (!this.opts.oidcPath) {
|
363
|
+
return {
|
364
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
365
|
+
error: 'server_error',
|
366
|
+
error_description: 'OpenID response handler path (oidcPath) is not set',
|
367
|
+
redirect_uri,
|
368
|
+
state,
|
369
|
+
}),
|
370
|
+
};
|
371
|
+
}
|
372
|
+
const { discoveryUrl, clientId, clientSecret } = connection.oidcProvider;
|
373
|
+
try {
|
374
|
+
const oidcIssuer = yield openid_client_1.Issuer.discover(discoveryUrl);
|
375
|
+
const oidcClient = new oidcIssuer.Client({
|
376
|
+
client_id: clientId,
|
377
|
+
client_secret: clientSecret,
|
378
|
+
redirect_uris: [this.opts.externalUrl + this.opts.oidcPath],
|
379
|
+
response_types: ['code'],
|
380
|
+
});
|
381
|
+
oidcCodeVerifier = openid_client_1.generators.codeVerifier();
|
382
|
+
const code_challenge = openid_client_1.generators.codeChallenge(oidcCodeVerifier);
|
383
|
+
ssoUrl = oidcClient.authorizationUrl({
|
384
|
+
scope: [...requestedScopes, 'openid', 'email', 'profile']
|
385
|
+
.filter((value, index, self) => self.indexOf(value) === index) // filter out duplicates
|
386
|
+
.join(' '),
|
387
|
+
code_challenge,
|
388
|
+
code_challenge_method: 'S256',
|
389
|
+
state: relayState,
|
390
|
+
});
|
391
|
+
}
|
392
|
+
catch (err) {
|
393
|
+
if (err) {
|
394
|
+
return {
|
395
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
396
|
+
error: 'server_error',
|
397
|
+
error_description: (err === null || err === void 0 ? void 0 : err.error) || (0, utils_1.getErrorMessage)(err),
|
398
|
+
redirect_uri,
|
399
|
+
state,
|
400
|
+
}),
|
401
|
+
};
|
316
402
|
}
|
317
403
|
}
|
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');
|
404
|
+
}
|
405
|
+
// Session persistence happens here
|
406
|
+
try {
|
331
407
|
const requested = { client_id, state, redirect_uri };
|
332
408
|
if (requestedTenant) {
|
333
409
|
requested.tenant = requestedTenant;
|
@@ -347,42 +423,58 @@ class OAuthController {
|
|
347
423
|
if (requestedScopes) {
|
348
424
|
requested.scope = requestedScopes;
|
349
425
|
}
|
350
|
-
|
351
|
-
id: samlReq.id,
|
426
|
+
const sessionObj = {
|
352
427
|
redirect_uri,
|
353
428
|
response_type,
|
354
429
|
state,
|
355
430
|
code_challenge,
|
356
431
|
code_challenge_method,
|
357
432
|
requested,
|
358
|
-
}
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
if (
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
433
|
+
};
|
434
|
+
yield this.sessionStore.put(sessionId, connectionIsSAML
|
435
|
+
? Object.assign(Object.assign({}, sessionObj), { id: samlReq === null || samlReq === void 0 ? void 0 : samlReq.id }) : Object.assign(Object.assign({}, sessionObj), { id: connection.clientID, oidcCodeVerifier }));
|
436
|
+
// Redirect to IdP
|
437
|
+
if (connectionIsSAML) {
|
438
|
+
let redirectUrl;
|
439
|
+
let authorizeForm;
|
440
|
+
if (!post) {
|
441
|
+
// HTTP Redirect binding
|
442
|
+
redirectUrl = redirect.success(ssoUrl, {
|
443
|
+
RelayState: relayState,
|
444
|
+
SAMLRequest: Buffer.from(yield deflateRawAsync(samlReq.request)).toString('base64'),
|
445
|
+
});
|
446
|
+
}
|
447
|
+
else {
|
448
|
+
// HTTP POST binding
|
449
|
+
authorizeForm = saml20_1.default.createPostForm(ssoUrl, [
|
450
|
+
{
|
451
|
+
name: 'RelayState',
|
452
|
+
value: relayState,
|
453
|
+
},
|
454
|
+
{
|
455
|
+
name: 'SAMLRequest',
|
456
|
+
value: Buffer.from(samlReq.request).toString('base64'),
|
457
|
+
},
|
458
|
+
]);
|
459
|
+
}
|
460
|
+
return {
|
461
|
+
redirect_url: redirectUrl,
|
462
|
+
authorize_form: authorizeForm,
|
463
|
+
};
|
464
|
+
}
|
465
|
+
else if (connectionIsOIDC) {
|
466
|
+
return { redirect_url: ssoUrl };
|
368
467
|
}
|
369
468
|
else {
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
value: Buffer.from(samlReq.request).toString('base64'),
|
379
|
-
},
|
380
|
-
]);
|
469
|
+
return {
|
470
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
471
|
+
error: 'invalid_request',
|
472
|
+
error_description: 'Connection appears to be misconfigured',
|
473
|
+
redirect_uri,
|
474
|
+
state,
|
475
|
+
}),
|
476
|
+
};
|
381
477
|
}
|
382
|
-
return {
|
383
|
-
redirect_url: redirectUrl,
|
384
|
-
authorize_form: authorizeForm,
|
385
|
-
};
|
386
478
|
}
|
387
479
|
catch (err) {
|
388
480
|
return {
|
@@ -412,22 +504,22 @@ class OAuthController {
|
|
412
504
|
if (!issuer) {
|
413
505
|
throw new error_1.JacksonError('Issuer not found.', 403);
|
414
506
|
}
|
415
|
-
const
|
507
|
+
const samlConnections = yield this.connectionStore.getByIndex({
|
416
508
|
name: utils_1.IndexNames.EntityID,
|
417
509
|
value: issuer,
|
418
510
|
});
|
419
|
-
if (!
|
420
|
-
throw new error_1.JacksonError('SAML
|
511
|
+
if (!samlConnections || samlConnections.length === 0) {
|
512
|
+
throw new error_1.JacksonError('SAML connection not found.', 403);
|
421
513
|
}
|
422
|
-
let
|
514
|
+
let samlConnection = samlConnections[0];
|
423
515
|
if (isIdPFlow) {
|
424
516
|
RelayState = '';
|
425
|
-
const {
|
517
|
+
const { resolvedConnection, app_select_form } = this.resolveMultipleConnectionMatches(samlConnections, idp_hint, { SAMLResponse }, true);
|
426
518
|
if (app_select_form) {
|
427
519
|
return { app_select_form };
|
428
520
|
}
|
429
|
-
if (
|
430
|
-
|
521
|
+
if (resolvedConnection) {
|
522
|
+
samlConnection = resolvedConnection;
|
431
523
|
}
|
432
524
|
}
|
433
525
|
let session;
|
@@ -439,33 +531,35 @@ class OAuthController {
|
|
439
531
|
}
|
440
532
|
if (!isIdPFlow) {
|
441
533
|
// Resolve if there are multiple matches for SP login. TODO: Support multiple matches for IdP login
|
442
|
-
|
443
|
-
|
444
|
-
?
|
445
|
-
:
|
534
|
+
samlConnection =
|
535
|
+
samlConnections.length === 1
|
536
|
+
? samlConnections[0]
|
537
|
+
: samlConnections.filter((c) => {
|
446
538
|
var _a, _b, _c;
|
447
539
|
return (c.clientID === ((_a = session === null || session === void 0 ? void 0 : session.requested) === null || _a === void 0 ? void 0 : _a.client_id) ||
|
448
540
|
(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
541
|
})[0];
|
450
542
|
}
|
451
|
-
if (!
|
452
|
-
throw new error_1.JacksonError('SAML
|
543
|
+
if (!samlConnection) {
|
544
|
+
throw new error_1.JacksonError('SAML connection not found.', 403);
|
453
545
|
}
|
454
546
|
const validateOpts = {
|
455
|
-
thumbprint:
|
547
|
+
thumbprint: samlConnection.idpMetadata.thumbprint,
|
456
548
|
audience: this.opts.samlAudience,
|
457
|
-
privateKey:
|
549
|
+
privateKey: samlConnection.certs.privateKey,
|
458
550
|
};
|
459
|
-
if (session &&
|
551
|
+
if (session &&
|
552
|
+
session.redirect_uri &&
|
553
|
+
!allowed.redirect(session.redirect_uri, samlConnection.redirectUrl)) {
|
460
554
|
throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
|
461
555
|
}
|
462
556
|
if (session && session.id) {
|
463
557
|
validateOpts.inResponseTo = session.id;
|
464
558
|
}
|
465
559
|
let profile;
|
466
|
-
const redirect_uri = (session && session.redirect_uri) ||
|
560
|
+
const redirect_uri = (session && session.redirect_uri) || samlConnection.defaultRedirectUrl;
|
467
561
|
try {
|
468
|
-
profile = yield
|
562
|
+
profile = yield validateSAMLResponse(rawResponse, validateOpts);
|
469
563
|
}
|
470
564
|
catch (err) {
|
471
565
|
// return error to redirect_uri
|
@@ -482,8 +576,8 @@ class OAuthController {
|
|
482
576
|
const code = crypto_1.default.randomBytes(20).toString('hex');
|
483
577
|
const codeVal = {
|
484
578
|
profile,
|
485
|
-
clientID:
|
486
|
-
clientSecret:
|
579
|
+
clientID: samlConnection.clientID,
|
580
|
+
clientSecret: samlConnection.clientSecret,
|
487
581
|
requested: session === null || session === void 0 ? void 0 : session.requested,
|
488
582
|
};
|
489
583
|
if (session) {
|
@@ -520,6 +614,127 @@ class OAuthController {
|
|
520
614
|
return { redirect_url: redirectUrl };
|
521
615
|
});
|
522
616
|
}
|
617
|
+
extractOIDCUserProfile(tokenSet, oidcClient) {
|
618
|
+
var _a, _b, _c;
|
619
|
+
return __awaiter(this, void 0, void 0, function* () {
|
620
|
+
const profile = { claims: {} };
|
621
|
+
const idTokenClaims = tokenSet.claims();
|
622
|
+
const userinfo = yield oidcClient.userinfo(tokenSet);
|
623
|
+
profile.claims.id = idTokenClaims.sub;
|
624
|
+
profile.claims.email = (_a = idTokenClaims.email) !== null && _a !== void 0 ? _a : userinfo.email;
|
625
|
+
profile.claims.firstName = (_b = idTokenClaims.given_name) !== null && _b !== void 0 ? _b : userinfo.given_name;
|
626
|
+
profile.claims.lastName = (_c = idTokenClaims.family_name) !== null && _c !== void 0 ? _c : userinfo.family_name;
|
627
|
+
profile.claims.raw = userinfo;
|
628
|
+
return profile;
|
629
|
+
});
|
630
|
+
}
|
631
|
+
oidcAuthzResponse(body) {
|
632
|
+
return __awaiter(this, void 0, void 0, function* () {
|
633
|
+
const { code: opCode, state, error, error_description } = body;
|
634
|
+
let RelayState = state || '';
|
635
|
+
if (!RelayState) {
|
636
|
+
throw new error_1.JacksonError('State from original request is missing.', 403);
|
637
|
+
}
|
638
|
+
RelayState = RelayState.replace(utils_1.relayStatePrefix, '');
|
639
|
+
const session = yield this.sessionStore.get(RelayState);
|
640
|
+
if (!session) {
|
641
|
+
throw new error_1.JacksonError('Unable to validate state from the original request.', 403);
|
642
|
+
}
|
643
|
+
const oidcConnection = yield this.connectionStore.get(session.id);
|
644
|
+
if (session.redirect_uri && !allowed.redirect(session.redirect_uri, oidcConnection.redirectUrl)) {
|
645
|
+
throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
|
646
|
+
}
|
647
|
+
const redirect_uri = (session && session.redirect_uri) || oidcConnection.defaultRedirectUrl;
|
648
|
+
if (error) {
|
649
|
+
return {
|
650
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
651
|
+
error,
|
652
|
+
error_description: error_description !== null && error_description !== void 0 ? error_description : 'Authorization failure at OIDC Provider',
|
653
|
+
redirect_uri,
|
654
|
+
state: session.state,
|
655
|
+
}),
|
656
|
+
};
|
657
|
+
}
|
658
|
+
if (!opCode) {
|
659
|
+
return {
|
660
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
661
|
+
error: 'server_error',
|
662
|
+
error_description: 'Authorization code could not be retrieved from OIDC Provider',
|
663
|
+
redirect_uri,
|
664
|
+
state: session.state,
|
665
|
+
}),
|
666
|
+
};
|
667
|
+
}
|
668
|
+
// Reconstruct the oidcClient
|
669
|
+
const { discoveryUrl, clientId, clientSecret } = oidcConnection.oidcProvider;
|
670
|
+
let profile;
|
671
|
+
try {
|
672
|
+
const oidcIssuer = yield openid_client_1.Issuer.discover(discoveryUrl);
|
673
|
+
const oidcClient = new oidcIssuer.Client({
|
674
|
+
client_id: clientId,
|
675
|
+
client_secret: clientSecret,
|
676
|
+
redirect_uris: [this.opts.externalUrl + this.opts.oidcPath],
|
677
|
+
response_types: ['code'],
|
678
|
+
});
|
679
|
+
const tokenSet = yield oidcClient.callback(this.opts.externalUrl + this.opts.oidcPath, {
|
680
|
+
code: opCode,
|
681
|
+
}, { code_verifier: session.oidcCodeVerifier });
|
682
|
+
profile = yield this.extractOIDCUserProfile(tokenSet, oidcClient);
|
683
|
+
}
|
684
|
+
catch (err) {
|
685
|
+
if (err) {
|
686
|
+
return {
|
687
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
688
|
+
error: 'server_error',
|
689
|
+
error_description: (err === null || err === void 0 ? void 0 : err.error) || (0, utils_1.getErrorMessage)(err),
|
690
|
+
redirect_uri,
|
691
|
+
state: session.state,
|
692
|
+
}),
|
693
|
+
};
|
694
|
+
}
|
695
|
+
}
|
696
|
+
// store details against a code
|
697
|
+
const code = crypto_1.default.randomBytes(20).toString('hex');
|
698
|
+
const codeVal = {
|
699
|
+
profile,
|
700
|
+
clientID: oidcConnection.clientID,
|
701
|
+
clientSecret: oidcConnection.clientSecret,
|
702
|
+
requested: session === null || session === void 0 ? void 0 : session.requested,
|
703
|
+
};
|
704
|
+
if (session) {
|
705
|
+
codeVal.session = session;
|
706
|
+
}
|
707
|
+
try {
|
708
|
+
yield this.codeStore.put(code, codeVal);
|
709
|
+
}
|
710
|
+
catch (err) {
|
711
|
+
// return error to redirect_uri
|
712
|
+
return {
|
713
|
+
redirect_url: (0, utils_1.OAuthErrorResponse)({
|
714
|
+
error: 'server_error',
|
715
|
+
error_description: (0, utils_1.getErrorMessage)(err),
|
716
|
+
redirect_uri,
|
717
|
+
state: session.state,
|
718
|
+
}),
|
719
|
+
};
|
720
|
+
}
|
721
|
+
const params = {
|
722
|
+
code,
|
723
|
+
};
|
724
|
+
if (session && session.state) {
|
725
|
+
params.state = session.state;
|
726
|
+
}
|
727
|
+
const redirectUrl = redirect.success(redirect_uri, params);
|
728
|
+
// delete the session
|
729
|
+
try {
|
730
|
+
yield this.sessionStore.delete(RelayState);
|
731
|
+
}
|
732
|
+
catch (_err) {
|
733
|
+
// ignore error
|
734
|
+
}
|
735
|
+
return { redirect_url: redirectUrl };
|
736
|
+
});
|
737
|
+
}
|
523
738
|
/**
|
524
739
|
* @swagger
|
525
740
|
*
|
@@ -541,13 +756,17 @@ class OAuthController {
|
|
541
756
|
* - name: client_id
|
542
757
|
* in: formData
|
543
758
|
* type: string
|
544
|
-
* description: Use the client_id returned by the SAML
|
759
|
+
* description: Use the client_id returned by the SAML connection API
|
545
760
|
* required: true
|
546
761
|
* - name: client_secret
|
547
762
|
* in: formData
|
548
763
|
* type: string
|
549
|
-
* description: Use the client_secret returned by the SAML
|
764
|
+
* description: Use the client_secret returned by the SAML connection API
|
550
765
|
* required: true
|
766
|
+
* - name: code_verifier
|
767
|
+
* in: formData
|
768
|
+
* type: string
|
769
|
+
* description: code_verifier against the code_challenge in the authz request (relevant to PKCE flow)
|
551
770
|
* - name: redirect_uri
|
552
771
|
* in: formData
|
553
772
|
* type: string
|
@@ -576,9 +795,12 @@ class OAuthController {
|
|
576
795
|
* expires_in: 300
|
577
796
|
*/
|
578
797
|
token(body) {
|
579
|
-
var _a, _b, _c, _d, _e;
|
798
|
+
var _a, _b, _c, _d, _e, _f;
|
580
799
|
return __awaiter(this, void 0, void 0, function* () {
|
581
|
-
const {
|
800
|
+
const { code, grant_type = 'authorization_code', redirect_uri } = body;
|
801
|
+
const client_id = 'client_id' in body ? body.client_id : undefined;
|
802
|
+
const client_secret = 'client_secret' in body ? body.client_secret : undefined;
|
803
|
+
const code_verifier = 'code_verifier' in body ? body.code_verifier : undefined;
|
582
804
|
metrics.increment('oauthToken');
|
583
805
|
if (grant_type !== 'authorization_code') {
|
584
806
|
throw new error_1.JacksonError('Unsupported grant_type', 400);
|
@@ -640,7 +862,7 @@ class OAuthController {
|
|
640
862
|
const requestedOIDCFlow = !!((_d = codeVal.requested) === null || _d === void 0 ? void 0 : _d.oidc);
|
641
863
|
const requestHasNonce = !!((_e = codeVal.requested) === null || _e === void 0 ? void 0 : _e.nonce);
|
642
864
|
if (requestedOIDCFlow) {
|
643
|
-
const { jwtSigningKeys, jwsAlg } = this.opts.openid;
|
865
|
+
const { jwtSigningKeys, jwsAlg } = (_f = this.opts.openid) !== null && _f !== void 0 ? _f : {};
|
644
866
|
if (!jwtSigningKeys || !(0, utils_1.isJWSKeyPairLoaded)(jwtSigningKeys)) {
|
645
867
|
throw new error_1.JacksonError('JWT signing keys are not loaded', 500);
|
646
868
|
}
|
@@ -700,11 +922,21 @@ class OAuthController {
|
|
700
922
|
* type: string
|
701
923
|
* lastName:
|
702
924
|
* type: string
|
925
|
+
* raw:
|
926
|
+
* type: object
|
927
|
+
* requested:
|
928
|
+
* type: object
|
703
929
|
* example:
|
704
930
|
* id: 32b5af58fdf
|
705
931
|
* email: jackson@coolstartup.com
|
706
932
|
* firstName: SAML
|
707
933
|
* lastName: Jackson
|
934
|
+
* raw: {
|
935
|
+
*
|
936
|
+
* }
|
937
|
+
* requested: {
|
938
|
+
*
|
939
|
+
* }
|
708
940
|
*/
|
709
941
|
userInfo(token) {
|
710
942
|
return __awaiter(this, void 0, void 0, function* () {
|