@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.
@@ -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 validateResponse = (rawResponse, validateOpts) => __awaiter(void 0, void 0, void 0, function* () {
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({ configStore, sessionStore, codeStore, tokenStore, opts }) {
88
- this.configStore = configStore;
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
- resolveMultipleConfigMatches(samlConfigs, idp_hint, originalParams, isIdpFlow = false) {
95
- if (samlConfigs.length > 1) {
95
+ resolveMultipleConnectionMatches(connections, idp_hint, originalParams, isIdpFlow = false) {
96
+ if (connections.length > 1) {
96
97
  if (idp_hint) {
97
- return { resolvedSamlConfig: samlConfigs.find(({ clientID }) => clientID === idp_hint) };
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 = samlConfigs.map(({ idpMetadata: { provider }, clientID }) => JSON.stringify({
103
- provider,
104
- clientID,
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
- const appList = samlConfigs.map(({ product, name, description, clientID }) => ({
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, tenant, product, access_type, resource, scope, nonce, code_challenge, code_challenge_method = '',
137
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
138
- provider = 'saml', idp_hint, prompt, } = body;
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 samlConfig;
155
+ let connection;
146
156
  const requestedScopes = getScopeValues(scope);
147
157
  const requestedOIDCFlow = requestedScopes.includes('openid');
148
158
  if (tenant && product) {
149
- const samlConfigs = yield this.configStore.getByIndex({
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 (!samlConfigs || samlConfigs.length === 0) {
154
- throw new error_1.JacksonError('SAML configuration not found.', 403);
163
+ if (!connections || connections.length === 0) {
164
+ throw new error_1.JacksonError('IdP connection not found.', 403);
155
165
  }
156
- samlConfig = samlConfigs[0];
166
+ connection = connections[0];
157
167
  // Support multiple matches
158
- const { resolvedSamlConfig, redirect_url } = this.resolveMultipleConfigMatches(samlConfigs, idp_hint, {
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 (resolvedSamlConfig) {
177
- samlConfig = resolvedSamlConfig;
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 config(s)
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 samlConfigs = yield this.configStore.getByIndex({
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 (!samlConfigs || samlConfigs.length === 0) {
203
- throw new error_1.JacksonError('SAML configuration not found.', 403);
211
+ if (!connections || connections.length === 0) {
212
+ throw new error_1.JacksonError('IdP connection not found.', 403);
204
213
  }
205
- samlConfig = samlConfigs[0];
214
+ connection = connections[0];
206
215
  // Support multiple matches
207
- const { resolvedSamlConfig, redirect_url } = this.resolveMultipleConfigMatches(samlConfigs, idp_hint, {
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 (resolvedSamlConfig) {
226
- samlConfig = resolvedSamlConfig;
233
+ if (resolvedConnection) {
234
+ connection = resolvedConnection;
227
235
  }
228
236
  }
229
237
  else {
230
- samlConfig = yield this.configStore.get(client_id);
231
- if (samlConfig) {
232
- requestedTenant = samlConfig.tenant;
233
- requestedProduct = samlConfig.product;
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 (!samlConfig) {
241
- throw new error_1.JacksonError('SAML configuration not found.', 403);
248
+ if (!connection) {
249
+ throw new error_1.JacksonError('IdP connection not found.', 403);
242
250
  }
243
- if (!allowed.redirect(redirect_uri, samlConfig.redirectUrl)) {
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 { sso } = samlConfig.idpMetadata;
278
- if ('redirectUrl' in sso) {
279
- // HTTP Redirect binding
280
- ssoUrl = sso.redirectUrl;
281
- }
282
- else if ('postUrl' in sso) {
283
- // HTTP-POST binding
284
- ssoUrl = sso.postUrl;
285
- post = true;
286
- }
287
- else {
288
- return {
289
- redirect_url: (0, utils_1.OAuthErrorResponse)({
290
- error: 'invalid_request',
291
- error_description: 'SAML binding could not be retrieved',
292
- redirect_uri,
293
- state,
294
- }),
295
- };
296
- }
297
- try {
298
- const { validTo } = new crypto_1.default.X509Certificate(samlConfig.certs.publicKey);
299
- const isValidExpiry = validTo != 'Bad time value' && new Date(validTo) > new Date();
300
- if (!isValidExpiry) {
301
- const certs = yield x509_1.default.generate();
302
- samlConfig.certs = certs;
303
- if (certs) {
304
- yield this.configStore.put(samlConfig.clientID, samlConfig, {
305
- // secondary index on entityID
306
- name: utils_1.IndexNames.EntityID,
307
- value: samlConfig.idpMetadata.entityID,
308
- }, {
309
- // secondary index on tenant + product
310
- name: utils_1.IndexNames.TenantProduct,
311
- value: dbutils.keyFromParts(samlConfig.tenant, samlConfig.product),
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
- else {
315
- throw new Error('Error generating x509 certs');
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
- // We will get undefined or Space delimited, case sensitive list of ASCII string values in prompt
319
- // If login is one of the value in prompt we want to enable forceAuthn
320
- // Else use the saml config forceAuthn value
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
- yield this.sessionStore.put(sessionId, {
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
- const relayState = utils_1.relayStatePrefix + sessionId;
360
- let redirectUrl;
361
- let authorizeForm;
362
- if (!post) {
363
- // HTTP Redirect binding
364
- redirectUrl = redirect.success(ssoUrl, {
365
- RelayState: relayState,
366
- SAMLRequest: Buffer.from(yield deflateRawAsync(samlReq.request)).toString('base64'),
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
- // HTTP POST binding
371
- authorizeForm = saml20_1.default.createPostForm(ssoUrl, [
372
- {
373
- name: 'RelayState',
374
- value: relayState,
375
- },
376
- {
377
- name: 'SAMLRequest',
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 samlConfigs = yield this.configStore.getByIndex({
507
+ const samlConnections = yield this.connectionStore.getByIndex({
416
508
  name: utils_1.IndexNames.EntityID,
417
509
  value: issuer,
418
510
  });
419
- if (!samlConfigs || samlConfigs.length === 0) {
420
- throw new error_1.JacksonError('SAML configuration not found.', 403);
511
+ if (!samlConnections || samlConnections.length === 0) {
512
+ throw new error_1.JacksonError('SAML connection not found.', 403);
421
513
  }
422
- let samlConfig = samlConfigs[0];
514
+ let samlConnection = samlConnections[0];
423
515
  if (isIdPFlow) {
424
516
  RelayState = '';
425
- const { resolvedSamlConfig, app_select_form } = this.resolveMultipleConfigMatches(samlConfigs, idp_hint, { SAMLResponse }, true);
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 (resolvedSamlConfig) {
430
- samlConfig = resolvedSamlConfig;
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
- samlConfig =
443
- samlConfigs.length === 1
444
- ? samlConfigs[0]
445
- : samlConfigs.filter((c) => {
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 (!samlConfig) {
452
- throw new error_1.JacksonError('SAML configuration not found.', 403);
543
+ if (!samlConnection) {
544
+ throw new error_1.JacksonError('SAML connection not found.', 403);
453
545
  }
454
546
  const validateOpts = {
455
- thumbprint: samlConfig.idpMetadata.thumbprint,
547
+ thumbprint: samlConnection.idpMetadata.thumbprint,
456
548
  audience: this.opts.samlAudience,
457
- privateKey: samlConfig.certs.privateKey,
549
+ privateKey: samlConnection.certs.privateKey,
458
550
  };
459
- if (session && session.redirect_uri && !allowed.redirect(session.redirect_uri, samlConfig.redirectUrl)) {
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) || samlConfig.defaultRedirectUrl;
560
+ const redirect_uri = (session && session.redirect_uri) || samlConnection.defaultRedirectUrl;
467
561
  try {
468
- profile = yield validateResponse(rawResponse, validateOpts);
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: samlConfig.clientID,
486
- clientSecret: samlConfig.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 config API
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 config API
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 { client_id, client_secret, code_verifier, code, grant_type = 'authorization_code', redirect_uri, } = body;
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* () {