@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.
@@ -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,
@@ -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, 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;
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 samlConfig;
154
+ let connection;
146
155
  const requestedScopes = getScopeValues(scope);
147
156
  const requestedOIDCFlow = requestedScopes.includes('openid');
148
157
  if (tenant && product) {
149
- const samlConfigs = yield this.configStore.getByIndex({
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 (!samlConfigs || samlConfigs.length === 0) {
154
- 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);
155
164
  }
156
- samlConfig = samlConfigs[0];
165
+ connection = connections[0];
157
166
  // Support multiple matches
158
- const { resolvedSamlConfig, redirect_url } = this.resolveMultipleConfigMatches(samlConfigs, idp_hint, {
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 (resolvedSamlConfig) {
177
- samlConfig = resolvedSamlConfig;
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 config(s)
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 samlConfigs = yield this.configStore.getByIndex({
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 (!samlConfigs || samlConfigs.length === 0) {
203
- 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);
204
212
  }
205
- samlConfig = samlConfigs[0];
213
+ connection = connections[0];
206
214
  // Support multiple matches
207
- const { resolvedSamlConfig, redirect_url } = this.resolveMultipleConfigMatches(samlConfigs, idp_hint, {
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 (resolvedSamlConfig) {
226
- samlConfig = resolvedSamlConfig;
232
+ if (resolvedConnection) {
233
+ connection = resolvedConnection;
227
234
  }
228
235
  }
229
236
  else {
230
- samlConfig = yield this.configStore.get(client_id);
231
- if (samlConfig) {
232
- requestedTenant = samlConfig.tenant;
233
- requestedProduct = samlConfig.product;
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 (!samlConfig) {
241
- throw new error_1.JacksonError('SAML configuration not found.', 403);
247
+ if (!connection) {
248
+ throw new error_1.JacksonError('IdP connection not found.', 403);
242
249
  }
243
- if (!allowed.redirect(redirect_uri, samlConfig.redirectUrl)) {
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 { 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
- });
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
- else {
315
- throw new Error('Error generating x509 certs');
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
- // 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');
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
- yield this.sessionStore.put(sessionId, {
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
- 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
- });
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
- // 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
- ]);
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 samlConfigs = yield this.configStore.getByIndex({
496
+ const samlConnections = yield this.connectionStore.getByIndex({
416
497
  name: utils_1.IndexNames.EntityID,
417
498
  value: issuer,
418
499
  });
419
- if (!samlConfigs || samlConfigs.length === 0) {
420
- 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);
421
502
  }
422
- let samlConfig = samlConfigs[0];
503
+ let samlConnection = samlConnections[0];
423
504
  if (isIdPFlow) {
424
505
  RelayState = '';
425
- 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);
426
507
  if (app_select_form) {
427
508
  return { app_select_form };
428
509
  }
429
- if (resolvedSamlConfig) {
430
- samlConfig = resolvedSamlConfig;
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
- samlConfig =
443
- samlConfigs.length === 1
444
- ? samlConfigs[0]
445
- : samlConfigs.filter((c) => {
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 (!samlConfig) {
452
- throw new error_1.JacksonError('SAML configuration not found.', 403);
532
+ if (!samlConnection) {
533
+ throw new error_1.JacksonError('SAML connection not found.', 403);
453
534
  }
454
535
  const validateOpts = {
455
- thumbprint: samlConfig.idpMetadata.thumbprint,
536
+ thumbprint: samlConnection.idpMetadata.thumbprint,
456
537
  audience: this.opts.samlAudience,
457
- privateKey: samlConfig.certs.privateKey,
538
+ privateKey: samlConnection.certs.privateKey,
458
539
  };
459
- 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)) {
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) || samlConfig.defaultRedirectUrl;
549
+ const redirect_uri = (session && session.redirect_uri) || samlConnection.defaultRedirectUrl;
467
550
  try {
468
- profile = yield validateResponse(rawResponse, validateOpts);
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: samlConfig.clientID,
486
- clientSecret: samlConfig.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 config API
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 config API
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 { 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;
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* () {