@boxyhq/saml-jackson 1.0.3 → 1.0.4

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.
@@ -61,9 +61,9 @@ const validateResponse = (rawResponse, validateOpts) => __awaiter(void 0, void 0
61
61
  }
62
62
  return profile;
63
63
  });
64
- function getEncodedTenantProduct(client_id) {
64
+ function getEncodedTenantProduct(param) {
65
65
  try {
66
- const sp = new URLSearchParams(client_id);
66
+ const sp = new URLSearchParams(param);
67
67
  const tenant = sp.get('tenant');
68
68
  const product = sp.get('product');
69
69
  if (tenant && product) {
@@ -128,7 +128,7 @@ class OAuthController {
128
128
  }
129
129
  authorize(body) {
130
130
  return __awaiter(this, void 0, void 0, function* () {
131
- const { response_type = 'code', client_id, redirect_uri, state, tenant, product, access_type, code_challenge, code_challenge_method = '',
131
+ const { response_type = 'code', client_id, redirect_uri, state, tenant, product, access_type, scope, code_challenge, code_challenge_method = '',
132
132
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
133
133
  provider = 'saml', idp_hint, } = body;
134
134
  let requestedTenant = tenant;
@@ -137,9 +137,6 @@ class OAuthController {
137
137
  if (!redirect_uri) {
138
138
  throw new error_1.JacksonError('Please specify a redirect URL.', 400);
139
139
  }
140
- if (!state) {
141
- throw new error_1.JacksonError('Please specify a state to safeguard against XSRF attacks.', 400);
142
- }
143
140
  let samlConfig;
144
141
  if (tenant && product) {
145
142
  const samlConfigs = yield this.configStore.getByIndex({
@@ -169,13 +166,15 @@ class OAuthController {
169
166
  samlConfig = resolvedSamlConfig;
170
167
  }
171
168
  }
172
- else if ((client_id && client_id !== '' && client_id !== 'undefined' && client_id !== 'null') ||
173
- (access_type && access_type !== '' && access_type !== 'undefined' && access_type !== 'null')) {
169
+ else if (client_id && client_id !== '' && client_id !== 'undefined' && client_id !== 'null') {
174
170
  // if tenant and product are encoded in the client_id then we parse it and check for the relevant config(s)
175
171
  let sp = getEncodedTenantProduct(client_id);
176
172
  if (!sp && access_type) {
177
173
  sp = getEncodedTenantProduct(access_type);
178
174
  }
175
+ if (!sp && scope) {
176
+ sp = getEncodedTenantProduct(scope);
177
+ }
179
178
  if (sp && sp.tenant && sp.product) {
180
179
  requestedTenant = sp.tenant;
181
180
  requestedProduct = sp.product;
@@ -223,6 +222,24 @@ class OAuthController {
223
222
  if (!allowed.redirect(redirect_uri, samlConfig.redirectUrl)) {
224
223
  throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
225
224
  }
225
+ if (!state) {
226
+ return {
227
+ redirect_url: (0, utils_1.OAuthErrorResponse)({
228
+ error: 'invalid_request',
229
+ error_description: 'Please specify a state to safeguard against XSRF attacks',
230
+ redirect_uri,
231
+ }),
232
+ };
233
+ }
234
+ if (response_type !== 'code') {
235
+ return {
236
+ redirect_url: (0, utils_1.OAuthErrorResponse)({
237
+ error: 'unsupported_response_type',
238
+ error_description: 'Only Authorization Code grant is supported',
239
+ redirect_uri,
240
+ }),
241
+ };
242
+ }
226
243
  let ssoUrl;
227
244
  let post = false;
228
245
  const { sso } = samlConfig.idpMetadata;
@@ -235,60 +252,80 @@ class OAuthController {
235
252
  ssoUrl = sso.postUrl;
236
253
  post = true;
237
254
  }
238
- const samlReq = saml20_1.default.request({
239
- ssoUrl,
240
- entityID: this.opts.samlAudience,
241
- callbackUrl: this.opts.externalUrl + this.opts.samlPath,
242
- signingKey: samlConfig.certs.privateKey,
243
- publicKey: samlConfig.certs.publicKey,
244
- });
245
- const sessionId = crypto_1.default.randomBytes(16).toString('hex');
246
- const requested = { client_id, state };
247
- if (requestedTenant) {
248
- requested.tenant = requestedTenant;
249
- }
250
- if (requestedProduct) {
251
- requested.product = requestedProduct;
255
+ else {
256
+ return {
257
+ redirect_url: (0, utils_1.OAuthErrorResponse)({
258
+ error: 'invalid_request',
259
+ error_description: 'SAML binding could not be retrieved',
260
+ redirect_uri,
261
+ }),
262
+ };
252
263
  }
253
- if (idp_hint) {
254
- requested.idp_hint = idp_hint;
255
- }
256
- yield this.sessionStore.put(sessionId, {
257
- id: samlReq.id,
258
- redirect_uri,
259
- response_type,
260
- state,
261
- code_challenge,
262
- code_challenge_method,
263
- requested,
264
- });
265
- const relayState = utils_1.relayStatePrefix + sessionId;
266
- let redirectUrl;
267
- let authorizeForm;
268
- if (!post) {
269
- // HTTP Redirect binding
270
- redirectUrl = redirect.success(ssoUrl, {
271
- RelayState: relayState,
272
- SAMLRequest: Buffer.from(yield deflateRawAsync(samlReq.request)).toString('base64'),
264
+ try {
265
+ const samlReq = saml20_1.default.request({
266
+ ssoUrl,
267
+ entityID: this.opts.samlAudience,
268
+ callbackUrl: this.opts.externalUrl + this.opts.samlPath,
269
+ signingKey: samlConfig.certs.privateKey,
270
+ publicKey: samlConfig.certs.publicKey,
273
271
  });
272
+ const sessionId = crypto_1.default.randomBytes(16).toString('hex');
273
+ const requested = { client_id, state };
274
+ if (requestedTenant) {
275
+ requested.tenant = requestedTenant;
276
+ }
277
+ if (requestedProduct) {
278
+ requested.product = requestedProduct;
279
+ }
280
+ if (idp_hint) {
281
+ requested.idp_hint = idp_hint;
282
+ }
283
+ yield this.sessionStore.put(sessionId, {
284
+ id: samlReq.id,
285
+ redirect_uri,
286
+ response_type,
287
+ state,
288
+ code_challenge,
289
+ code_challenge_method,
290
+ requested,
291
+ });
292
+ const relayState = utils_1.relayStatePrefix + sessionId;
293
+ let redirectUrl;
294
+ let authorizeForm;
295
+ if (!post) {
296
+ // HTTP Redirect binding
297
+ redirectUrl = redirect.success(ssoUrl, {
298
+ RelayState: relayState,
299
+ SAMLRequest: Buffer.from(yield deflateRawAsync(samlReq.request)).toString('base64'),
300
+ });
301
+ }
302
+ else {
303
+ // HTTP POST binding
304
+ authorizeForm = saml20_1.default.createPostForm(ssoUrl, [
305
+ {
306
+ name: 'RelayState',
307
+ value: relayState,
308
+ },
309
+ {
310
+ name: 'SAMLRequest',
311
+ value: Buffer.from(samlReq.request).toString('base64'),
312
+ },
313
+ ]);
314
+ }
315
+ return {
316
+ redirect_url: redirectUrl,
317
+ authorize_form: authorizeForm,
318
+ };
319
+ }
320
+ catch (err) {
321
+ return {
322
+ redirect_url: (0, utils_1.OAuthErrorResponse)({
323
+ error: 'server_error',
324
+ error_description: (0, utils_1.getErrorMessage)(err),
325
+ redirect_uri,
326
+ }),
327
+ };
274
328
  }
275
- else {
276
- // HTTP POST binding
277
- authorizeForm = saml20_1.default.createPostForm(ssoUrl, [
278
- {
279
- name: 'RelayState',
280
- value: relayState,
281
- },
282
- {
283
- name: 'SAMLRequest',
284
- value: Buffer.from(samlReq.request).toString('base64'),
285
- },
286
- ]);
287
- }
288
- return {
289
- redirect_url: redirectUrl,
290
- authorize_form: authorizeForm,
291
- };
292
329
  });
293
330
  }
294
331
  samlResponse(body) {
@@ -346,10 +383,27 @@ class OAuthController {
346
383
  thumbprint: samlConfig.idpMetadata.thumbprint,
347
384
  audience: this.opts.samlAudience,
348
385
  };
386
+ if (session && session.redirect_uri && !allowed.redirect(session.redirect_uri, samlConfig.redirectUrl)) {
387
+ throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
388
+ }
349
389
  if (session && session.id) {
350
390
  validateOpts.inResponseTo = session.id;
351
391
  }
352
- const profile = yield validateResponse(rawResponse, validateOpts);
392
+ let profile;
393
+ const redirect_uri = (session && session.redirect_uri) || samlConfig.defaultRedirectUrl;
394
+ try {
395
+ profile = yield validateResponse(rawResponse, validateOpts);
396
+ }
397
+ catch (err) {
398
+ // return error to redirect_uri
399
+ return {
400
+ redirect_url: (0, utils_1.OAuthErrorResponse)({
401
+ error: 'access_denied',
402
+ error_description: (0, utils_1.getErrorMessage)(err),
403
+ redirect_uri,
404
+ }),
405
+ };
406
+ }
353
407
  // store details against a code
354
408
  const code = crypto_1.default.randomBytes(20).toString('hex');
355
409
  const codeVal = {
@@ -361,9 +415,18 @@ class OAuthController {
361
415
  if (session) {
362
416
  codeVal.session = session;
363
417
  }
364
- yield this.codeStore.put(code, codeVal);
365
- if (session && session.redirect_uri && !allowed.redirect(session.redirect_uri, samlConfig.redirectUrl)) {
366
- throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
418
+ try {
419
+ yield this.codeStore.put(code, codeVal);
420
+ }
421
+ catch (err) {
422
+ // return error to redirect_uri
423
+ return {
424
+ redirect_url: (0, utils_1.OAuthErrorResponse)({
425
+ error: 'server_error',
426
+ error_description: (0, utils_1.getErrorMessage)(err),
427
+ redirect_uri,
428
+ }),
429
+ };
367
430
  }
368
431
  const params = {
369
432
  code,
@@ -371,7 +434,7 @@ class OAuthController {
371
434
  if (session && session.state) {
372
435
  params.state = session.state;
373
436
  }
374
- const redirectUrl = redirect.success((session && session.redirect_uri) || samlConfig.defaultRedirectUrl, params);
437
+ const redirectUrl = redirect.success(redirect_uri, params);
375
438
  // delete the session
376
439
  try {
377
440
  yield this.sessionStore.delete(RelayState);
@@ -1,6 +1,9 @@
1
+ import type { OAuthErrorHandlerParams } from '../typings';
1
2
  export declare enum IndexNames {
2
3
  EntityID = "entityID",
3
4
  TenantProduct = "tenantProduct"
4
5
  }
5
6
  export declare const relayStatePrefix = "boxyhq_jackson_";
6
7
  export declare const validateAbsoluteUrl: (url: any, message: any) => void;
8
+ export declare const OAuthErrorResponse: ({ error, error_description, redirect_uri }: OAuthErrorHandlerParams) => string;
9
+ export declare function getErrorMessage(error: unknown): string;
@@ -1,7 +1,31 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.validateAbsoluteUrl = exports.relayStatePrefix = exports.IndexNames = void 0;
26
+ exports.getErrorMessage = exports.OAuthErrorResponse = exports.validateAbsoluteUrl = exports.relayStatePrefix = exports.IndexNames = void 0;
4
27
  const error_1 = require("./error");
28
+ const redirect = __importStar(require("./oauth/redirect"));
5
29
  var IndexNames;
6
30
  (function (IndexNames) {
7
31
  IndexNames["EntityID"] = "entityID";
@@ -17,3 +41,14 @@ const validateAbsoluteUrl = (url, message) => {
17
41
  }
18
42
  };
19
43
  exports.validateAbsoluteUrl = validateAbsoluteUrl;
44
+ const OAuthErrorResponse = ({ error, error_description, redirect_uri }) => {
45
+ return redirect.success(redirect_uri, { error, error_description });
46
+ };
47
+ exports.OAuthErrorResponse = OAuthErrorResponse;
48
+ // https://kentcdodds.com/blog/get-a-catch-block-error-message-with-typescript
49
+ function getErrorMessage(error) {
50
+ if (error instanceof Error)
51
+ return error.message;
52
+ return String(error);
53
+ }
54
+ exports.getErrorMessage = getErrorMessage;
package/dist/typings.d.ts CHANGED
@@ -52,6 +52,7 @@ export interface OAuthReqBody {
52
52
  tenant?: string;
53
53
  product?: string;
54
54
  access_type?: string;
55
+ scope?: string;
55
56
  code_challenge: string;
56
57
  code_challenge_method: 'plain' | 'S256' | '';
57
58
  provider: 'saml';
@@ -79,6 +80,7 @@ export interface Profile {
79
80
  email: string;
80
81
  firstName: string;
81
82
  lastName: string;
83
+ requested: Record<string, string>;
82
84
  }
83
85
  export interface Index {
84
86
  name: string;
@@ -160,4 +162,9 @@ export interface ILogoutController {
160
162
  }>;
161
163
  handleResponse(body: SAMLResponsePayload): Promise<any>;
162
164
  }
165
+ export interface OAuthErrorHandlerParams {
166
+ error: 'invalid_request' | 'access_denied' | 'unauthorized_client' | 'unsupported_response_type' | 'invalid_scope' | 'server_error' | 'temporarily_unavailable';
167
+ error_description: string;
168
+ redirect_uri: string;
169
+ }
163
170
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxyhq/saml-jackson",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "SAML Jackson library",
5
5
  "keywords": [
6
6
  "SAML 2.0"
@@ -38,12 +38,12 @@
38
38
  "dependencies": {
39
39
  "@boxyhq/saml20": "1.0.2",
40
40
  "@opentelemetry/api-metrics": "0.27.0",
41
- "@peculiar/webcrypto": "1.3.3",
42
- "@peculiar/x509": "1.6.1",
43
- "mongodb": "4.5.0",
41
+ "@peculiar/webcrypto": "1.4.0",
42
+ "@peculiar/x509": "1.6.3",
43
+ "mongodb": "4.6.0",
44
44
  "mysql2": "2.3.3",
45
45
  "pg": "8.7.3",
46
- "redis": "4.1.0",
46
+ "redis": "4.0.6",
47
47
  "reflect-metadata": "0.1.13",
48
48
  "ripemd160": "2.0.2",
49
49
  "typeorm": "0.3.6",
@@ -51,11 +51,11 @@
51
51
  "xmlbuilder": "15.1.1"
52
52
  },
53
53
  "devDependencies": {
54
- "@types/node": "17.0.31",
54
+ "@types/node": "17.0.34",
55
55
  "@types/sinon": "10.0.11",
56
56
  "@types/tap": "15.0.7",
57
- "@typescript-eslint/eslint-plugin": "5.23.0",
58
- "@typescript-eslint/parser": "5.23.0",
57
+ "@typescript-eslint/eslint-plugin": "5.25.0",
58
+ "@typescript-eslint/parser": "5.25.0",
59
59
  "cross-env": "7.0.3",
60
60
  "eslint": "8.15.0",
61
61
  "eslint-config-prettier": "8.5.0",