@boxyhq/saml-jackson 0.2.0 → 0.2.1-beta.156

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.
package/README.md CHANGED
@@ -236,6 +236,8 @@ https://localhost:5000/oauth/authorize
236
236
 
237
237
  - response_type=code: This is the only supported type for now but maybe extended in the future
238
238
  - client_id: Use the client_id returned by the SAML config API or use `tenant=<tenantID>&product=<productID>` to use the tenant and product IDs instead. **Note:** Please don't forget to URL encode the query parameters including `client_id`.
239
+ - tenant: Optionally you can provide a dummy `client_id` and specify the `tenant` and `product` custom attributes (if your OAuth 2.0 library allows it).
240
+ - product: Should be specified if specifying `tenant` above
239
241
  - redirect_uri: This is where the user will be taken back once the authorization flow is complete
240
242
  - state: Use a randomly generated string as the state, this will be echoed back as a query parameter when taking the user back to the `redirect_uri` above. You should validate the state to prevent XSRF attacks
241
243
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxyhq/saml-jackson",
3
- "version": "0.2.0",
3
+ "version": "0.2.1-beta.156",
4
4
  "license": "Apache 2.0",
5
5
  "description": "SAML 2.0 service",
6
6
  "main": "src/index.js",
@@ -17,9 +17,9 @@
17
17
  "scripts": {
18
18
  "start": "cross-env IDP_ENABLED=true node src/jackson.js",
19
19
  "dev": "cross-env IDP_ENABLED=true nodemon src/jackson.js",
20
- "mongo": "cross-env DB_ENGINE=mongo DB_URL=mongodb://localhost:27017/jackson nodemon src/jackson.js",
21
- "pre-loaded": "cross-env DB_ENGINE=mem PRE_LOADED_CONFIG='./_config' nodemon src/jackson.js",
22
- "pre-loaded-db": "cross-env PRE_LOADED_CONFIG='./_config' nodemon src/jackson.js",
20
+ "mongo": "cross-env JACKSON_API_KEYS=secret DB_ENGINE=mongo DB_URL=mongodb://localhost:27017/jackson nodemon src/jackson.js",
21
+ "pre-loaded": "cross-env JACKSON_API_KEYS=secret DB_ENGINE=mem PRE_LOADED_CONFIG='./_config' nodemon src/jackson.js",
22
+ "pre-loaded-db": "cross-env JACKSON_API_KEYS=secret PRE_LOADED_CONFIG='./_config' nodemon src/jackson.js",
23
23
  "test": "tap --timeout=100 src/**/*.test.js",
24
24
  "dev-dbs": "docker-compose -f ./_dev/docker-compose.yml up -d",
25
25
  "dev-dbs-destroy": "docker-compose -f ./_dev/docker-compose.yml down --volumes --remove-orphans"
@@ -64,4 +64,4 @@
64
64
  "*.js": "eslint --cache --fix",
65
65
  "*.{js,css,md}": "prettier --write"
66
66
  }
67
- }
67
+ }
@@ -10,7 +10,7 @@ let configStore;
10
10
  const extractHostName = (url) => {
11
11
  try {
12
12
  const pUrl = new URL(url);
13
- if(pUrl.hostname.startsWith('www.')) {
13
+ if (pUrl.hostname.startsWith('www.')) {
14
14
  return pUrl.hostname.substring(4);
15
15
  }
16
16
  return pUrl.hostname;
@@ -56,7 +56,7 @@ const config = async (body) => {
56
56
  {
57
57
  idpMetadata,
58
58
  defaultRedirectUrl,
59
- redirectUrl: JSON.parse(redirectUrl),
59
+ redirectUrl: JSON.parse(redirectUrl), // redirectUrl is a stringified array
60
60
  tenant,
61
61
  product,
62
62
  clientID,
@@ -61,7 +61,19 @@ const authorize = async (body) => {
61
61
 
62
62
  let samlConfig;
63
63
 
64
- if (
64
+ if (tenant && product) {
65
+ const samlConfigs = await configStore.getByIndex({
66
+ name: indexNames.tenantProduct,
67
+ value: dbutils.keyFromParts(tenant, product),
68
+ });
69
+
70
+ if (!samlConfigs || samlConfigs.length === 0) {
71
+ throw new JacksonError('SAML configuration not found.', 403);
72
+ }
73
+
74
+ // TODO: Support multiple matches
75
+ samlConfig = samlConfigs[0];
76
+ } else if (
65
77
  client_id &&
66
78
  client_id !== '' &&
67
79
  client_id !== 'undefined' &&
@@ -85,17 +97,10 @@ const authorize = async (body) => {
85
97
  samlConfig = await configStore.get(client_id);
86
98
  }
87
99
  } else {
88
- const samlConfigs = await configStore.getByIndex({
89
- name: indexNames.tenantProduct,
90
- value: dbutils.keyFromParts(tenant, product),
91
- });
92
-
93
- if (!samlConfigs || samlConfigs.length === 0) {
94
- throw new JacksonError('SAML configuration not found.', 403);
95
- }
96
-
97
- // TODO: Support multiple matches
98
- samlConfig = samlConfigs[0];
100
+ throw new JacksonError(
101
+ 'You need to specify client_id or tenant & product',
102
+ 403
103
+ );
99
104
  }
100
105
 
101
106
  if (!samlConfig) {
@@ -253,14 +258,16 @@ const token = async (body) => {
253
258
 
254
259
  if (client_id && client_secret) {
255
260
  // check if we have an encoded client_id
256
- const sp = getEncodedClientId(client_id);
257
- if (!sp) {
258
- // OAuth flow
259
- if (
260
- client_id !== codeVal.clientID ||
261
- client_secret !== codeVal.clientSecret
262
- ) {
263
- throw new JacksonError('Invalid client_id or client_secret', 401);
261
+ if (client_id !== 'dummy' && client_secret !== 'dummy') {
262
+ const sp = getEncodedClientId(client_id);
263
+ if (!sp) {
264
+ // OAuth flow
265
+ if (
266
+ client_id !== codeVal.clientID ||
267
+ client_secret !== codeVal.clientSecret
268
+ ) {
269
+ throw new JacksonError('Invalid client_id or client_secret', 401);
270
+ }
264
271
  }
265
272
  }
266
273
  } else if (code_verifier) {
package/src/db/db.js CHANGED
@@ -3,18 +3,39 @@ const mongo = require('./mongo.js');
3
3
  const redis = require('./redis.js');
4
4
  const sql = require('./sql/sql.js');
5
5
  const store = require('./store.js');
6
+ const encrypter = require('./encrypter.js');
7
+
8
+ const decrypt = (res, encryptionKey) => {
9
+ if (res.iv && res.tag) {
10
+ return JSON.parse(
11
+ encrypter.decrypt(res.value, res.iv, res.tag, encryptionKey)
12
+ );
13
+ }
14
+
15
+ return JSON.parse(res.value);
16
+ };
6
17
 
7
18
  class DB {
8
- constructor(db) {
19
+ constructor(db, encryptionKey) {
9
20
  this.db = db;
21
+ this.encryptionKey = encryptionKey;
10
22
  }
11
23
 
12
24
  async get(namespace, key) {
13
- return await this.db.get(namespace, key);
25
+ const res = await this.db.get(namespace, key);
26
+ if (!res) {
27
+ return null;
28
+ }
29
+
30
+ return decrypt(res, this.encryptionKey);
14
31
  }
15
32
 
16
33
  async getByIndex(namespace, idx) {
17
- return await this.db.getByIndex(namespace, idx);
34
+ const res = await this.db.getByIndex(namespace, idx);
35
+ const encryptionKey = this.encryptionKey;
36
+ return res.map((r) => {
37
+ return decrypt(r, encryptionKey);
38
+ });
18
39
  }
19
40
 
20
41
  // ttl is in seconds
@@ -23,7 +44,11 @@ class DB {
23
44
  throw new Error('secondary indexes not allow on a store with ttl');
24
45
  }
25
46
 
26
- return await this.db.put(namespace, key, val, ttl, ...indexes);
47
+ const dbVal = this.encryptionKey
48
+ ? encrypter.encrypt(JSON.stringify(val), this.encryptionKey)
49
+ : { value: JSON.stringify(val) };
50
+
51
+ return await this.db.put(namespace, key, dbVal, ttl, ...indexes);
27
52
  }
28
53
 
29
54
  async delete(namespace, key) {
@@ -37,15 +62,18 @@ class DB {
37
62
 
38
63
  module.exports = {
39
64
  new: async (options) => {
65
+ const encryptionKey = options.encryptionKey
66
+ ? Buffer.from(options.encryptionKey, 'latin1')
67
+ : null;
40
68
  switch (options.engine) {
41
69
  case 'redis':
42
- return new DB(await redis.new(options));
70
+ return new DB(await redis.new(options), encryptionKey);
43
71
  case 'sql':
44
- return new DB(await sql.new(options));
72
+ return new DB(await sql.new(options), encryptionKey);
45
73
  case 'mongo':
46
- return new DB(await mongo.new(options));
74
+ return new DB(await mongo.new(options), encryptionKey);
47
75
  case 'mem':
48
- return new DB(await mem.new(options));
76
+ return new DB(await mem.new(options), encryptionKey);
49
77
  default:
50
78
  throw new Error('unsupported db engine: ' + options.engine);
51
79
  }
package/src/db/db.test.js CHANGED
@@ -2,6 +2,8 @@ const t = require('tap');
2
2
 
3
3
  const DB = require('./db.js');
4
4
 
5
+ const encryptionKey = '3yGrTcnKPBqqHoH3zZMAU6nt4bmIYb2q';
6
+
5
7
  let configStores = [];
6
8
  let ttlStores = [];
7
9
  const ttl = 3;
@@ -17,39 +19,87 @@ const record2 = {
17
19
  city: 'London',
18
20
  };
19
21
 
22
+ const memDbConfig = {
23
+ engine: 'mem',
24
+ ttl: 1,
25
+ };
26
+
27
+ const redisDbConfig = {
28
+ engine: 'redis',
29
+ url: 'redis://localhost:6379',
30
+ };
31
+
32
+ const postgresDbConfig = {
33
+ engine: 'sql',
34
+ url: 'postgresql://postgres:postgres@localhost:5432/postgres',
35
+ type: 'postgres',
36
+ ttl: 1,
37
+ limit: 1,
38
+ };
39
+
40
+ const mongoDbConfig = {
41
+ engine: 'mongo',
42
+ url: 'mongodb://localhost:27017/jackson',
43
+ };
44
+
45
+ const mysqlDbConfig = {
46
+ engine: 'sql',
47
+ url: 'mysql://root:mysql@localhost:3307/mysql',
48
+ type: 'mysql',
49
+ ttl: 1,
50
+ limit: 1,
51
+ };
52
+
53
+ const mariadbDbConfig = {
54
+ engine: 'sql',
55
+ url: 'mariadb://root@localhost:3306/mysql',
56
+ type: 'mariadb',
57
+ ttl: 1,
58
+ limit: 1,
59
+ };
60
+
20
61
  const dbs = [
21
62
  {
22
- engine: 'mem',
23
- ttl: 1,
63
+ ...memDbConfig,
64
+ },
65
+ {
66
+ ...memDbConfig,
67
+ encryptionKey,
68
+ },
69
+ {
70
+ ...redisDbConfig,
71
+ },
72
+ {
73
+ ...redisDbConfig,
74
+ encryptionKey,
75
+ },
76
+ {
77
+ ...postgresDbConfig,
78
+ },
79
+ {
80
+ ...postgresDbConfig,
81
+ encryptionKey,
82
+ },
83
+ {
84
+ ...mongoDbConfig,
24
85
  },
25
86
  {
26
- engine: 'redis',
27
- url: 'redis://localhost:6379',
87
+ ...mongoDbConfig,
88
+ encryptionKey,
28
89
  },
29
90
  {
30
- engine: 'sql',
31
- url: 'postgresql://postgres:postgres@localhost:5432/postgres',
32
- type: 'postgres',
33
- ttl: 1,
34
- limit: 1,
91
+ ...mysqlDbConfig,
35
92
  },
36
93
  {
37
- engine: 'mongo',
38
- url: 'mongodb://localhost:27017/jackson',
94
+ ...mysqlDbConfig,
95
+ encryptionKey,
39
96
  },
40
97
  {
41
- engine: 'sql',
42
- url: 'mysql://root:mysql@localhost:3307/mysql',
43
- type: 'mysql',
44
- ttl: 1,
45
- limit: 1,
98
+ ...mariadbDbConfig,
46
99
  },
47
100
  {
48
- engine: 'sql',
49
- url: 'mariadb://root@localhost:3306/mysql',
50
- type: 'mariadb',
51
- ttl: 1,
52
- limit: 1,
101
+ ...mariadbDbConfig,
102
+ encryptionKey,
53
103
  },
54
104
  ];
55
105
 
@@ -224,7 +274,7 @@ t.test('dbs', ({ end }) => {
224
274
  }
225
275
 
226
276
  await new Promise((resolve) =>
227
- setTimeout(resolve, (2*ttl + 0.5) * 1000)
277
+ setTimeout(resolve, (2 * ttl + 0.5) * 1000)
228
278
  );
229
279
 
230
280
  const ret1 = await ttlStore.get(record1.id);
@@ -0,0 +1,36 @@
1
+ const crypto = require('crypto');
2
+
3
+ const ALGO = 'aes-256-gcm';
4
+ const BLOCK_SIZE = 16; // 128 bit
5
+
6
+ const encrypt = (text, key) => {
7
+ const iv = crypto.randomBytes(BLOCK_SIZE);
8
+ const cipher = crypto.createCipheriv(ALGO, key, iv);
9
+
10
+ let ciphertext = cipher.update(text, 'utf8', 'base64');
11
+ ciphertext += cipher.final('base64');
12
+ return {
13
+ iv: iv.toString('base64'),
14
+ tag: cipher.getAuthTag().toString('base64'),
15
+ value: ciphertext,
16
+ };
17
+ };
18
+
19
+ const decrypt = (ciphertext, iv, tag, key) => {
20
+ const decipher = crypto.createDecipheriv(
21
+ ALGO,
22
+ key,
23
+ Buffer.from(iv, 'base64')
24
+ );
25
+ decipher.setAuthTag(Buffer.from(tag, 'base64'));
26
+
27
+ let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
28
+ cleartext += decipher.final('utf8');
29
+
30
+ return cleartext;
31
+ };
32
+
33
+ module.exports = {
34
+ encrypt,
35
+ decrypt,
36
+ };
package/src/db/mem.js CHANGED
@@ -34,7 +34,7 @@ class Mem {
34
34
  async get(namespace, key) {
35
35
  let res = this.store[dbutils.key(namespace, key)];
36
36
  if (res) {
37
- return JSON.parse(res);
37
+ return res;
38
38
  }
39
39
 
40
40
  return null;
@@ -54,7 +54,7 @@ class Mem {
54
54
  async put(namespace, key, val, ttl = 0, ...indexes) {
55
55
  const k = dbutils.key(namespace, key);
56
56
 
57
- this.store[k] = JSON.stringify(val);
57
+ this.store[k] = val;
58
58
 
59
59
  if (ttl) {
60
60
  this.ttlStore[k] = {
package/src/db/mongo.js CHANGED
@@ -25,7 +25,7 @@ class Mongo {
25
25
  _id: dbutils.key(namespace, key),
26
26
  });
27
27
  if (res && res.value) {
28
- return JSON.parse(res.value);
28
+ return res.value;
29
29
  }
30
30
 
31
31
  return null;
@@ -40,7 +40,7 @@ class Mongo {
40
40
 
41
41
  const ret = [];
42
42
  for (const doc of docs || []) {
43
- ret.push(JSON.parse(doc.value));
43
+ ret.push(doc.value);
44
44
  }
45
45
 
46
46
  return ret;
@@ -48,7 +48,7 @@ class Mongo {
48
48
 
49
49
  async put(namespace, key, val, ttl = 0, ...indexes) {
50
50
  const doc = {
51
- value: JSON.stringify(val),
51
+ value: val,
52
52
  };
53
53
 
54
54
  if (ttl) {
@@ -27,6 +27,16 @@ module.exports = (type) => {
27
27
  value: {
28
28
  type: valueType(type),
29
29
  },
30
+ iv: {
31
+ type: 'varchar',
32
+ length: 64,
33
+ nullable: true,
34
+ },
35
+ tag: {
36
+ type: 'varchar',
37
+ length: 64,
38
+ nullable: true,
39
+ },
30
40
  },
31
41
  });
32
42
  };
@@ -1,7 +1,9 @@
1
1
  /*export */ class JacksonStore {
2
- constructor(key, value) {
2
+ constructor(key, value, iv, tag) {
3
3
  this.key = key;
4
4
  this.value = value;
5
+ this.iv = iv;
6
+ this.tag = tag;
5
7
  }
6
8
  }
7
9
 
package/src/db/sql/sql.js CHANGED
@@ -1,3 +1,5 @@
1
+ /*eslint no-constant-condition: ["error", { "checkLoops": false }]*/
2
+
1
3
  require('reflect-metadata');
2
4
  const typeorm = require('typeorm');
3
5
  const JacksonStore = require('./model/JacksonStore.js');
@@ -12,7 +14,7 @@ class Sql {
12
14
  while (true) {
13
15
  try {
14
16
  this.connection = await typeorm.createConnection({
15
- name: options.type,
17
+ name: options.type + Math.floor(Math.random() * 100000),
16
18
  type: options.type,
17
19
  url: options.url,
18
20
  synchronize: true,
@@ -79,8 +81,12 @@ class Sql {
79
81
  key: dbutils.key(namespace, key),
80
82
  });
81
83
 
82
- if (res) {
83
- return JSON.parse(res.value);
84
+ if (res && res.value) {
85
+ return {
86
+ value: res.value,
87
+ iv: res.iv,
88
+ tag: res.tag,
89
+ };
84
90
  }
85
91
 
86
92
  return null;
@@ -95,21 +101,21 @@ class Sql {
95
101
 
96
102
  if (res) {
97
103
  res.forEach((r) => {
98
- ret.push(JSON.parse(r.store.value));
104
+ ret.push({
105
+ value: r.store.value,
106
+ iv: r.store.iv,
107
+ tag: r.store.tag,
108
+ });
99
109
  });
100
110
  }
101
111
 
102
- if (res && res.store) {
103
- return JSON.parse(res.store.value);
104
- }
105
-
106
112
  return ret;
107
113
  }
108
114
 
109
115
  async put(namespace, key, val, ttl = 0, ...indexes) {
110
116
  await this.connection.transaction(async (transactionalEntityManager) => {
111
117
  const dbKey = dbutils.key(namespace, key);
112
- const store = new JacksonStore(dbKey, JSON.stringify(val));
118
+ const store = new JacksonStore(dbKey, val.value, val.iv, val.tag);
113
119
  await transactionalEntityManager.save(store);
114
120
 
115
121
  if (ttl) {
package/src/env.js CHANGED
@@ -18,6 +18,7 @@ const db = {
18
18
  url: process.env.DB_URL,
19
19
  type: process.env.DB_TYPE,
20
20
  ttl: process.env.DB_TTL,
21
+ encryptionKey: process.env.DB_ENCRYPTION_KEY,
21
22
  };
22
23
 
23
24
  module.exports = {