@boxyhq/saml-jackson 1.2.2 → 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"));
@@ -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* () {