@boxyhq/saml-jackson 1.2.1 → 1.3.0

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