@boxyhq/saml-jackson 1.0.2 → 1.0.5

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.
@@ -50,7 +50,7 @@ const redirect = __importStar(require("./oauth/redirect"));
50
50
  const utils_1 = require("./utils");
51
51
  const deflateRawAsync = (0, util_1.promisify)(zlib_1.deflateRaw);
52
52
  const validateResponse = (rawResponse, validateOpts) => __awaiter(void 0, void 0, void 0, function* () {
53
- const profile = yield saml20_1.default.validateAsync(rawResponse, validateOpts);
53
+ const profile = yield saml20_1.default.validate(rawResponse, validateOpts);
54
54
  if (profile && profile.claims) {
55
55
  // we map claims to our attributes id, email, firstName, lastName where possible. We also map original claims to raw
56
56
  profile.claims = claims_1.default.map(profile.claims);
@@ -61,9 +61,9 @@ const validateResponse = (rawResponse, validateOpts) => __awaiter(void 0, void 0
61
61
  }
62
62
  return profile;
63
63
  });
64
- function getEncodedClientId(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, 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({
@@ -171,7 +168,13 @@ class OAuthController {
171
168
  }
172
169
  else if (client_id && client_id !== '' && client_id !== 'undefined' && client_id !== 'null') {
173
170
  // if tenant and product are encoded in the client_id then we parse it and check for the relevant config(s)
174
- const sp = getEncodedClientId(client_id);
171
+ let sp = getEncodedTenantProduct(client_id);
172
+ if (!sp && access_type) {
173
+ sp = getEncodedTenantProduct(access_type);
174
+ }
175
+ if (!sp && scope) {
176
+ sp = getEncodedTenantProduct(scope);
177
+ }
175
178
  if (sp && sp.tenant && sp.product) {
176
179
  requestedTenant = sp.tenant;
177
180
  requestedProduct = sp.product;
@@ -204,6 +207,10 @@ class OAuthController {
204
207
  }
205
208
  else {
206
209
  samlConfig = yield this.configStore.get(client_id);
210
+ if (samlConfig) {
211
+ requestedTenant = samlConfig.tenant;
212
+ requestedProduct = samlConfig.product;
213
+ }
207
214
  }
208
215
  }
209
216
  else {
@@ -215,6 +222,25 @@ class OAuthController {
215
222
  if (!allowed.redirect(redirect_uri, samlConfig.redirectUrl)) {
216
223
  throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
217
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
+ state,
241
+ }),
242
+ };
243
+ }
218
244
  let ssoUrl;
219
245
  let post = false;
220
246
  const { sso } = samlConfig.idpMetadata;
@@ -227,63 +253,86 @@ class OAuthController {
227
253
  ssoUrl = sso.postUrl;
228
254
  post = true;
229
255
  }
230
- const samlReq = saml20_1.default.request({
231
- ssoUrl,
232
- entityID: this.opts.samlAudience,
233
- callbackUrl: this.opts.externalUrl + this.opts.samlPath,
234
- signingKey: samlConfig.certs.privateKey,
235
- publicKey: samlConfig.certs.publicKey,
236
- });
237
- const sessionId = crypto_1.default.randomBytes(16).toString('hex');
238
- const requested = { client_id, state };
239
- if (requestedTenant) {
240
- requested.tenant = requestedTenant;
241
- }
242
- if (requestedProduct) {
243
- requested.product = requestedProduct;
256
+ else {
257
+ return {
258
+ redirect_url: (0, utils_1.OAuthErrorResponse)({
259
+ error: 'invalid_request',
260
+ error_description: 'SAML binding could not be retrieved',
261
+ redirect_uri,
262
+ state,
263
+ }),
264
+ };
244
265
  }
245
- if (idp_hint) {
246
- requested.idp_hint = idp_hint;
247
- }
248
- yield this.sessionStore.put(sessionId, {
249
- id: samlReq.id,
250
- redirect_uri,
251
- response_type,
252
- state,
253
- code_challenge,
254
- code_challenge_method,
255
- requested,
256
- });
257
- const relayState = utils_1.relayStatePrefix + sessionId;
258
- let redirectUrl;
259
- let authorizeForm;
260
- if (!post) {
261
- // HTTP Redirect binding
262
- redirectUrl = redirect.success(ssoUrl, {
263
- RelayState: relayState,
264
- SAMLRequest: Buffer.from(yield deflateRawAsync(samlReq.request)).toString('base64'),
266
+ try {
267
+ const samlReq = saml20_1.default.request({
268
+ ssoUrl,
269
+ entityID: this.opts.samlAudience,
270
+ callbackUrl: this.opts.externalUrl + this.opts.samlPath,
271
+ signingKey: samlConfig.certs.privateKey,
272
+ publicKey: samlConfig.certs.publicKey,
265
273
  });
274
+ const sessionId = crypto_1.default.randomBytes(16).toString('hex');
275
+ const requested = { client_id, state };
276
+ if (requestedTenant) {
277
+ requested.tenant = requestedTenant;
278
+ }
279
+ if (requestedProduct) {
280
+ requested.product = requestedProduct;
281
+ }
282
+ if (idp_hint) {
283
+ requested.idp_hint = idp_hint;
284
+ }
285
+ yield this.sessionStore.put(sessionId, {
286
+ id: samlReq.id,
287
+ redirect_uri,
288
+ response_type,
289
+ state,
290
+ code_challenge,
291
+ code_challenge_method,
292
+ requested,
293
+ });
294
+ const relayState = utils_1.relayStatePrefix + sessionId;
295
+ let redirectUrl;
296
+ let authorizeForm;
297
+ if (!post) {
298
+ // HTTP Redirect binding
299
+ redirectUrl = redirect.success(ssoUrl, {
300
+ RelayState: relayState,
301
+ SAMLRequest: Buffer.from(yield deflateRawAsync(samlReq.request)).toString('base64'),
302
+ });
303
+ }
304
+ else {
305
+ // HTTP POST binding
306
+ authorizeForm = saml20_1.default.createPostForm(ssoUrl, [
307
+ {
308
+ name: 'RelayState',
309
+ value: relayState,
310
+ },
311
+ {
312
+ name: 'SAMLRequest',
313
+ value: Buffer.from(samlReq.request).toString('base64'),
314
+ },
315
+ ]);
316
+ }
317
+ return {
318
+ redirect_url: redirectUrl,
319
+ authorize_form: authorizeForm,
320
+ };
321
+ }
322
+ catch (err) {
323
+ return {
324
+ redirect_url: (0, utils_1.OAuthErrorResponse)({
325
+ error: 'server_error',
326
+ error_description: (0, utils_1.getErrorMessage)(err),
327
+ redirect_uri,
328
+ state,
329
+ }),
330
+ };
266
331
  }
267
- else {
268
- // HTTP POST binding
269
- authorizeForm = saml20_1.default.createPostForm(ssoUrl, [
270
- {
271
- name: 'RelayState',
272
- value: relayState,
273
- },
274
- {
275
- name: 'SAMLRequest',
276
- value: Buffer.from(samlReq.request).toString('base64'),
277
- },
278
- ]);
279
- }
280
- return {
281
- redirect_url: redirectUrl,
282
- authorize_form: authorizeForm,
283
- };
284
332
  });
285
333
  }
286
334
  samlResponse(body) {
335
+ var _a, _b;
287
336
  return __awaiter(this, void 0, void 0, function* () {
288
337
  const { SAMLResponse, idp_hint } = body;
289
338
  let RelayState = body.RelayState || ''; // RelayState will contain the sessionId from earlier quasi-oauth flow
@@ -294,10 +343,13 @@ class OAuthController {
294
343
  }
295
344
  RelayState = RelayState.replace(utils_1.relayStatePrefix, '');
296
345
  const rawResponse = Buffer.from(SAMLResponse, 'base64').toString();
297
- const parsedResp = yield saml20_1.default.parseAsync(rawResponse);
346
+ const issuer = saml20_1.default.parseIssuer(rawResponse);
347
+ if (!issuer) {
348
+ throw new error_1.JacksonError('Issuer not found.', 403);
349
+ }
298
350
  const samlConfigs = yield this.configStore.getByIndex({
299
351
  name: utils_1.IndexNames.EntityID,
300
- value: parsedResp === null || parsedResp === void 0 ? void 0 : parsedResp.issuer,
352
+ value: issuer,
301
353
  });
302
354
  if (!samlConfigs || samlConfigs.length === 0) {
303
355
  throw new error_1.JacksonError('SAML configuration not found.', 403);
@@ -337,11 +389,30 @@ class OAuthController {
337
389
  const validateOpts = {
338
390
  thumbprint: samlConfig.idpMetadata.thumbprint,
339
391
  audience: this.opts.samlAudience,
392
+ privateKey: samlConfig.certs.privateKey,
340
393
  };
394
+ if (session && session.redirect_uri && !allowed.redirect(session.redirect_uri, samlConfig.redirectUrl)) {
395
+ throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
396
+ }
341
397
  if (session && session.id) {
342
398
  validateOpts.inResponseTo = session.id;
343
399
  }
344
- const profile = yield validateResponse(rawResponse, validateOpts);
400
+ let profile;
401
+ const redirect_uri = (session && session.redirect_uri) || samlConfig.defaultRedirectUrl;
402
+ try {
403
+ profile = yield validateResponse(rawResponse, validateOpts);
404
+ }
405
+ catch (err) {
406
+ // return error to redirect_uri
407
+ return {
408
+ redirect_url: (0, utils_1.OAuthErrorResponse)({
409
+ error: 'access_denied',
410
+ error_description: (0, utils_1.getErrorMessage)(err),
411
+ redirect_uri,
412
+ state: (_a = session === null || session === void 0 ? void 0 : session.requested) === null || _a === void 0 ? void 0 : _a.state,
413
+ }),
414
+ };
415
+ }
345
416
  // store details against a code
346
417
  const code = crypto_1.default.randomBytes(20).toString('hex');
347
418
  const codeVal = {
@@ -353,9 +424,19 @@ class OAuthController {
353
424
  if (session) {
354
425
  codeVal.session = session;
355
426
  }
356
- yield this.codeStore.put(code, codeVal);
357
- if (session && session.redirect_uri && !allowed.redirect(session.redirect_uri, samlConfig.redirectUrl)) {
358
- throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
427
+ try {
428
+ yield this.codeStore.put(code, codeVal);
429
+ }
430
+ catch (err) {
431
+ // return error to redirect_uri
432
+ return {
433
+ redirect_url: (0, utils_1.OAuthErrorResponse)({
434
+ error: 'server_error',
435
+ error_description: (0, utils_1.getErrorMessage)(err),
436
+ redirect_uri,
437
+ state: (_b = session === null || session === void 0 ? void 0 : session.requested) === null || _b === void 0 ? void 0 : _b.state,
438
+ }),
439
+ };
359
440
  }
360
441
  const params = {
361
442
  code,
@@ -363,7 +444,7 @@ class OAuthController {
363
444
  if (session && session.state) {
364
445
  params.state = session.state;
365
446
  }
366
- const redirectUrl = redirect.success((session && session.redirect_uri) || samlConfig.defaultRedirectUrl, params);
447
+ const redirectUrl = redirect.success(redirect_uri, params);
367
448
  // delete the session
368
449
  try {
369
450
  yield this.sessionStore.delete(RelayState);
@@ -456,7 +537,7 @@ class OAuthController {
456
537
  else if (client_id && client_secret) {
457
538
  // check if we have an encoded client_id
458
539
  if (client_id !== 'dummy') {
459
- const sp = getEncodedClientId(client_id);
540
+ const sp = getEncodedTenantProduct(client_id);
460
541
  if (!sp) {
461
542
  // OAuth flow
462
543
  if (client_id !== codeVal.clientID || client_secret !== codeVal.clientSecret) {
@@ -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, state, }: 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, state, }) => {
45
+ return redirect.success(redirect_uri, { error, error_description, state });
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
@@ -51,6 +51,8 @@ export interface OAuthReqBody {
51
51
  state: string;
52
52
  tenant?: string;
53
53
  product?: string;
54
+ access_type?: string;
55
+ scope?: string;
54
56
  code_challenge: string;
55
57
  code_challenge_method: 'plain' | 'S256' | '';
56
58
  provider: 'saml';
@@ -78,6 +80,7 @@ export interface Profile {
78
80
  email: string;
79
81
  firstName: string;
80
82
  lastName: string;
83
+ requested: Record<string, string>;
81
84
  }
82
85
  export interface Index {
83
86
  name: string;
@@ -141,7 +144,7 @@ interface Metadata {
141
144
  };
142
145
  entityID: string;
143
146
  thumbprint: string;
144
- loginType: 'idp';
147
+ loginType: 'idp' | 'sp';
145
148
  provider: string;
146
149
  }
147
150
  export interface SAMLConfig {
@@ -159,4 +162,10 @@ export interface ILogoutController {
159
162
  }>;
160
163
  handleResponse(body: SAMLResponsePayload): Promise<any>;
161
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
+ state?: string;
170
+ }
162
171
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxyhq/saml-jackson",
3
- "version": "1.0.2",
3
+ "version": "1.0.5",
4
4
  "description": "SAML Jackson library",
5
5
  "keywords": [
6
6
  "SAML 2.0"
@@ -36,35 +36,36 @@
36
36
  "statements": 70
37
37
  },
38
38
  "dependencies": {
39
- "@boxyhq/saml20": "1.0.2",
39
+ "@boxyhq/saml20": "1.0.3",
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
+ "@opentelemetry/api": "1.0.4",
42
+ "@peculiar/webcrypto": "1.4.0",
43
+ "@peculiar/x509": "1.7.2",
44
+ "mongodb": "4.7.0",
44
45
  "mysql2": "2.3.3",
45
46
  "pg": "8.7.3",
46
47
  "redis": "4.0.6",
47
48
  "reflect-metadata": "0.1.13",
48
49
  "ripemd160": "2.0.2",
49
- "typeorm": "0.3.6",
50
+ "typeorm": "0.3.7",
50
51
  "xml2js": "0.4.23",
51
52
  "xmlbuilder": "15.1.1"
52
53
  },
53
54
  "devDependencies": {
54
- "@types/node": "17.0.30",
55
- "@types/sinon": "10.0.11",
55
+ "@types/node": "18.0.1",
56
+ "@types/sinon": "10.0.12",
56
57
  "@types/tap": "15.0.7",
57
- "@typescript-eslint/eslint-plugin": "5.21.0",
58
- "@typescript-eslint/parser": "5.21.0",
58
+ "@typescript-eslint/eslint-plugin": "5.30.5",
59
+ "@typescript-eslint/parser": "5.30.5",
59
60
  "cross-env": "7.0.3",
60
- "eslint": "8.14.0",
61
+ "eslint": "8.19.0",
61
62
  "eslint-config-prettier": "8.5.0",
62
- "prettier": "2.6.2",
63
- "sinon": "13.0.2",
64
- "tap": "16.1.0",
65
- "ts-node": "10.7.0",
66
- "tsconfig-paths": "3.14.1",
67
- "typescript": "4.6.4"
63
+ "prettier": "2.7.1",
64
+ "sinon": "14.0.0",
65
+ "tap": "16.3.0",
66
+ "ts-node": "10.8.2",
67
+ "tsconfig-paths": "4.0.0",
68
+ "typescript": "4.7.4"
68
69
  },
69
70
  "engines": {
70
71
  "node": ">=14.18.1 <=16.x"