@boxyhq/saml-jackson 0.2.1-beta.156 → 0.2.1-beta.162
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 +33 -16
- package/package.json +1 -1
- package/src/controller/api.js +22 -0
- package/src/db/db.test.js +3 -3
- package/src/db/sql/sql.js +3 -3
- package/src/index.js +3 -2
- package/src/jackson.js +2 -2
- package/src/test/api.test.js +161 -0
package/README.md
CHANGED
@@ -71,6 +71,20 @@ router.post('/api/v1/saml/config', async (req, res) => {
|
|
71
71
|
});
|
72
72
|
}
|
73
73
|
});
|
74
|
+
// fetch config
|
75
|
+
router.get('/api/v1/saml/config', async (req, res) => {
|
76
|
+
try {
|
77
|
+
// apply your authentication flow (or ensure this route has passed through your auth middleware)
|
78
|
+
...
|
79
|
+
|
80
|
+
// only when properly authenticated, call the config function
|
81
|
+
res.json(await apiController.config(req.query));
|
82
|
+
} catch (err) {
|
83
|
+
res.status(500).json({
|
84
|
+
error: err.message,
|
85
|
+
});
|
86
|
+
}
|
87
|
+
});
|
74
88
|
|
75
89
|
// OAuth 2.0 flow
|
76
90
|
router.get('/oauth/authorize', async (req, res) => {
|
@@ -198,7 +212,7 @@ The response returns a JSON with `client_id` and `client_secret` that can be sto
|
|
198
212
|
This endpoint can be used to return metadata about an existing SAML config. This can be used to check and display the details to your customers. You can use either `clientID` or `tenant` and `product` combination.
|
199
213
|
|
200
214
|
```
|
201
|
-
curl --location
|
215
|
+
curl -G --location 'http://localhost:6000/api/v1/saml/config' \
|
202
216
|
--header 'Authorization: Api-Key <Jackson API Key>' \
|
203
217
|
--header 'Content-Type: application/x-www-form-urlencoded' \
|
204
218
|
--data-urlencode 'tenant=boxyhq.com' \
|
@@ -206,7 +220,7 @@ curl --location --request POST 'http://localhost:6000/api/v1/saml/config/get' \
|
|
206
220
|
```
|
207
221
|
|
208
222
|
```
|
209
|
-
curl --location
|
223
|
+
curl -G --location 'http://localhost:6000/api/v1/saml/config' \
|
210
224
|
--header 'Authorization: Api-Key <Jackson API Key>' \
|
211
225
|
--header 'Content-Type: application/x-www-form-urlencoded' \
|
212
226
|
--data-urlencode 'clientID=<Client ID>'
|
@@ -321,20 +335,23 @@ Configuration is done via env vars (and in the case of the npm library via an op
|
|
321
335
|
|
322
336
|
The following options are supported and will have to be configured during deployment.
|
323
337
|
|
324
|
-
| Key
|
325
|
-
|
|
326
|
-
| HOST_URL
|
327
|
-
| HOST_PORT
|
328
|
-
| EXTERNAL_URL (npm: externalUrl)
|
329
|
-
| INTERNAL_HOST_URL
|
330
|
-
| INTERNAL_HOST_PORT
|
331
|
-
| JACKSON_API_KEYS
|
332
|
-
| SAML_AUDIENCE (npm: samlAudience)
|
333
|
-
| IDP_ENABLED (npm: idpEnabled)
|
334
|
-
| DB_ENGINE (npm: db.engine)
|
335
|
-
| DB_URL (npm: db.url)
|
336
|
-
| DB_TYPE (npm: db.type)
|
337
|
-
|
|
338
|
+
| Key | Description | Default |
|
339
|
+
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
|
340
|
+
| HOST_URL | The URL to bind to | `localhost` |
|
341
|
+
| HOST_PORT | The port to bind to | `5000` |
|
342
|
+
| EXTERNAL_URL (npm: externalUrl) | The public URL to reach this service, used internally for documenting the SAML configuration instructions. | `http://{HOST_URL}:{HOST_PORT}` |
|
343
|
+
| INTERNAL_HOST_URL | The URL to bind to expose the internal APIs. Do not configure this to a public network. | `localhost` |
|
344
|
+
| INTERNAL_HOST_PORT | The port to bind to for the internal APIs. | `6000` |
|
345
|
+
| JACKSON_API_KEYS | A comma separated list of API keys that will be validated when serving the Config API requests | |
|
346
|
+
| SAML_AUDIENCE (npm: samlAudience) | This is just an identifier to validate the SAML audience, this value will also get configured in the SAML apps created by your customers. Once set do not change this value unless you get your customers to reconfigure their SAML again. It is case-sensitive. This does not have to be a real URL. | `https://saml.boxyhq.com` |
|
347
|
+
| IDP_ENABLED (npm: idpEnabled) | Set to `true` to enable IdP initiated login for SAML. SP initiated login is the only recommended flow but you might have to support IdP login at times. | `false` |
|
348
|
+
| DB_ENGINE (npm: db.engine) | Supported values are `redis`, `sql`, `mongo`, `mem`. | `sql` |
|
349
|
+
| DB_URL (npm: db.url) | The database URL to connect to. For example `postgres://postgres:postgres@localhost:5450/jackson` | |
|
350
|
+
| DB_TYPE (npm: db.type) | Only needed when DB_ENGINE is `sql`. Supported values are `postgres`, `cockroachdb`, `mysql`, `mariadb`. | `postgres` |
|
351
|
+
| DB_TTL (npm: db.ttl) | TTL for the code, session and token stores (in seconds). | 300 |
|
352
|
+
| DB_CLEANUP_LIMIT (npm: db.cleanupLimit) | Limit cleanup of TTL entries to this number. | 1000 |
|
353
|
+
| DB_ENCRYPTION_KEY (npm: db.encryptionKey) | To encrypt data at rest specify a 32 character key. | |
|
354
|
+
| PRE_LOADED_CONFIG | If you only need a single tenant or a handful of pre-configured tenants then this config will help you read and load SAML configs. It works well with the mem DB engine so you don't have to configure any external databases for this to work (though it works with those as well). This is a path (absolute or relative) to a directory that contains files organized in the format described in the next section. | |
|
338
355
|
|
339
356
|
## Pre-loaded SAML Configuration
|
340
357
|
|
package/package.json
CHANGED
package/src/controller/api.js
CHANGED
@@ -2,6 +2,7 @@ const saml = require('../saml/saml.js');
|
|
2
2
|
const x509 = require('../saml/x509.js');
|
3
3
|
const dbutils = require('../db/utils.js');
|
4
4
|
const { indexNames } = require('./utils.js');
|
5
|
+
const { JacksonError } = require('./error.js');
|
5
6
|
|
6
7
|
const crypto = require('crypto');
|
7
8
|
|
@@ -22,6 +23,27 @@ const extractHostName = (url) => {
|
|
22
23
|
const config = async (body) => {
|
23
24
|
const { rawMetadata, defaultRedirectUrl, redirectUrl, tenant, product } =
|
24
25
|
body;
|
26
|
+
|
27
|
+
if (!rawMetadata) {
|
28
|
+
throw new JacksonError('Please provide rawMetadata', 400);
|
29
|
+
}
|
30
|
+
|
31
|
+
if (!defaultRedirectUrl) {
|
32
|
+
throw new JacksonError('Please provide a defaultRedirectUrl', 400);
|
33
|
+
}
|
34
|
+
|
35
|
+
if (!redirectUrl) {
|
36
|
+
throw new JacksonError('Please provide redirectUrl', 400);
|
37
|
+
}
|
38
|
+
|
39
|
+
if (!tenant) {
|
40
|
+
throw new JacksonError('Please provide tenant', 400);
|
41
|
+
}
|
42
|
+
|
43
|
+
if (!product) {
|
44
|
+
throw new JacksonError('Please provide product', 400);
|
45
|
+
}
|
46
|
+
|
25
47
|
const idpMetadata = await saml.parseMetadataAsync(rawMetadata);
|
26
48
|
|
27
49
|
// extract provider
|
package/src/db/db.test.js
CHANGED
@@ -34,7 +34,7 @@ const postgresDbConfig = {
|
|
34
34
|
url: 'postgresql://postgres:postgres@localhost:5432/postgres',
|
35
35
|
type: 'postgres',
|
36
36
|
ttl: 1,
|
37
|
-
|
37
|
+
cleanupLimit: 1,
|
38
38
|
};
|
39
39
|
|
40
40
|
const mongoDbConfig = {
|
@@ -47,7 +47,7 @@ const mysqlDbConfig = {
|
|
47
47
|
url: 'mysql://root:mysql@localhost:3307/mysql',
|
48
48
|
type: 'mysql',
|
49
49
|
ttl: 1,
|
50
|
-
|
50
|
+
cleanupLimit: 1,
|
51
51
|
};
|
52
52
|
|
53
53
|
const mariadbDbConfig = {
|
@@ -55,7 +55,7 @@ const mariadbDbConfig = {
|
|
55
55
|
url: 'mariadb://root@localhost:3306/mysql',
|
56
56
|
type: 'mariadb',
|
57
57
|
ttl: 1,
|
58
|
-
|
58
|
+
cleanupLimit: 1,
|
59
59
|
};
|
60
60
|
|
61
61
|
const dbs = [
|
package/src/db/sql/sql.js
CHANGED
@@ -39,14 +39,14 @@ class Sql {
|
|
39
39
|
this.indexRepository = this.connection.getRepository(JacksonIndex);
|
40
40
|
this.ttlRepository = this.connection.getRepository(JacksonTTL);
|
41
41
|
|
42
|
-
if (options.ttl && options.
|
42
|
+
if (options.ttl && options.cleanupLimit) {
|
43
43
|
this.ttlCleanup = async () => {
|
44
44
|
const now = Date.now();
|
45
45
|
|
46
46
|
while (true) {
|
47
47
|
const ids = await this.ttlRepository
|
48
48
|
.createQueryBuilder('jackson_ttl')
|
49
|
-
.limit(options.
|
49
|
+
.limit(options.cleanupLimit)
|
50
50
|
.where('jackson_ttl.expiresAt <= :expiresAt', { expiresAt: now })
|
51
51
|
.getMany();
|
52
52
|
|
@@ -68,7 +68,7 @@ class Sql {
|
|
68
68
|
this.timerId = setTimeout(this.ttlCleanup, options.ttl * 1000);
|
69
69
|
} else {
|
70
70
|
console.log(
|
71
|
-
'Warning: ttl cleanup not enabled, set both "ttl" and "
|
71
|
+
'Warning: ttl cleanup not enabled, set both "ttl" and "cleanupLimit" options to enable it!'
|
72
72
|
);
|
73
73
|
}
|
74
74
|
|
package/src/index.js
CHANGED
@@ -22,7 +22,7 @@ const defaultOpts = (opts) => {
|
|
22
22
|
newOpts.db.url || 'postgresql://postgres:postgres@localhost:5432/postgres';
|
23
23
|
newOpts.db.type = newOpts.db.type || 'postgres'; // Only needed if DB_ENGINE is sql. Supported values: postgres, cockroachdb, mysql, mariadb
|
24
24
|
newOpts.db.ttl = (newOpts.db.ttl || 300) * 1; // TTL for the code, session and token stores (in seconds)
|
25
|
-
newOpts.db.
|
25
|
+
newOpts.db.cleanupLimit = (newOpts.db.cleanupLimit || 1000) * 1; // Limit cleanup of TTL entries to this many items at a time
|
26
26
|
|
27
27
|
return newOpts;
|
28
28
|
};
|
@@ -56,7 +56,8 @@ module.exports = async function (opts) {
|
|
56
56
|
}
|
57
57
|
}
|
58
58
|
|
59
|
-
const type =
|
59
|
+
const type =
|
60
|
+
opts.db.engine === 'sql' && opts.db.type ? ' Type: ' + opts.db.type : '';
|
60
61
|
console.log(`Using engine: ${opts.db.engine}.${type}`);
|
61
62
|
|
62
63
|
return {
|
package/src/jackson.js
CHANGED
@@ -114,7 +114,7 @@ internalApp.post(apiPath + '/config', async (req, res) => {
|
|
114
114
|
}
|
115
115
|
});
|
116
116
|
|
117
|
-
internalApp.
|
117
|
+
internalApp.get(apiPath + '/config', async (req, res) => {
|
118
118
|
try {
|
119
119
|
const apiKey = extractAuthToken(req);
|
120
120
|
if (!validateApiKey(apiKey)) {
|
@@ -122,7 +122,7 @@ internalApp.post(apiPath + '/config/get', async (req, res) => {
|
|
122
122
|
return;
|
123
123
|
}
|
124
124
|
|
125
|
-
res.json(await apiController.getConfig(req.
|
125
|
+
res.json(await apiController.getConfig(req.query));
|
126
126
|
} catch (err) {
|
127
127
|
res.status(500).json({
|
128
128
|
error: err.message,
|
@@ -0,0 +1,161 @@
|
|
1
|
+
const tap = require('tap');
|
2
|
+
const path = require('path');
|
3
|
+
const sinon = require('sinon');
|
4
|
+
const crypto = require('crypto');
|
5
|
+
|
6
|
+
const readConfig = require('../read-config');
|
7
|
+
const dbutils = require('../db/utils');
|
8
|
+
|
9
|
+
let apiController;
|
10
|
+
|
11
|
+
const options = {
|
12
|
+
externalUrl: 'https://my-cool-app.com',
|
13
|
+
samlAudience: 'https://saml.boxyhq.com',
|
14
|
+
samlPath: '/sso/oauth/saml',
|
15
|
+
db: {
|
16
|
+
engine: 'mem',
|
17
|
+
},
|
18
|
+
};
|
19
|
+
|
20
|
+
tap.before(async () => {
|
21
|
+
const controller = await require('../index.js')(options);
|
22
|
+
|
23
|
+
apiController = controller.apiController;
|
24
|
+
});
|
25
|
+
|
26
|
+
tap.teardown(async () => {
|
27
|
+
process.exit(0);
|
28
|
+
});
|
29
|
+
|
30
|
+
tap.test('controller/api', async (t) => {
|
31
|
+
const metadataPath = path.join(__dirname, '/data/metadata');
|
32
|
+
const config = await readConfig(metadataPath);
|
33
|
+
|
34
|
+
t.test('.config()', async (t) => {
|
35
|
+
t.test('when required fields are missing or invalid', async (t) => {
|
36
|
+
t.test('when `rawMetadata` is empty', async (t) => {
|
37
|
+
const body = Object.assign({}, config[0]);
|
38
|
+
delete body['rawMetadata'];
|
39
|
+
|
40
|
+
try {
|
41
|
+
await apiController.config(body);
|
42
|
+
t.fail('Expecting JacksonError.');
|
43
|
+
} catch (err) {
|
44
|
+
t.equal(err.message, 'Please provide rawMetadata');
|
45
|
+
t.equal(err.statusCode, 400);
|
46
|
+
}
|
47
|
+
|
48
|
+
t.end();
|
49
|
+
});
|
50
|
+
|
51
|
+
t.test('when `defaultRedirectUrl` is empty', async (t) => {
|
52
|
+
const body = Object.assign({}, config[0]);
|
53
|
+
delete body['defaultRedirectUrl'];
|
54
|
+
|
55
|
+
try {
|
56
|
+
await apiController.config(body);
|
57
|
+
t.fail('Expecting JacksonError.');
|
58
|
+
} catch (err) {
|
59
|
+
t.equal(err.message, 'Please provide a defaultRedirectUrl');
|
60
|
+
t.equal(err.statusCode, 400);
|
61
|
+
}
|
62
|
+
|
63
|
+
t.end();
|
64
|
+
});
|
65
|
+
|
66
|
+
t.test('when `redirectUrl` is empty', async (t) => {
|
67
|
+
const body = Object.assign({}, config[0]);
|
68
|
+
delete body['redirectUrl'];
|
69
|
+
|
70
|
+
try {
|
71
|
+
await apiController.config(body);
|
72
|
+
t.fail('Expecting JacksonError.');
|
73
|
+
} catch (err) {
|
74
|
+
t.equal(err.message, 'Please provide redirectUrl');
|
75
|
+
t.equal(err.statusCode, 400);
|
76
|
+
}
|
77
|
+
|
78
|
+
t.end();
|
79
|
+
});
|
80
|
+
|
81
|
+
t.test('when `tenant` is empty', async (t) => {
|
82
|
+
const body = Object.assign({}, config[0]);
|
83
|
+
delete body['tenant'];
|
84
|
+
|
85
|
+
try {
|
86
|
+
await apiController.config(body);
|
87
|
+
t.fail('Expecting JacksonError.');
|
88
|
+
} catch (err) {
|
89
|
+
t.equal(err.message, 'Please provide tenant');
|
90
|
+
t.equal(err.statusCode, 400);
|
91
|
+
}
|
92
|
+
|
93
|
+
t.end();
|
94
|
+
});
|
95
|
+
|
96
|
+
t.test('when `product` is empty', async (t) => {
|
97
|
+
const body = Object.assign({}, config[0]);
|
98
|
+
delete body['product'];
|
99
|
+
|
100
|
+
try {
|
101
|
+
await apiController.config(body);
|
102
|
+
t.fail('Expecting JacksonError.');
|
103
|
+
} catch (err) {
|
104
|
+
t.equal(err.message, 'Please provide product');
|
105
|
+
t.equal(err.statusCode, 400);
|
106
|
+
}
|
107
|
+
|
108
|
+
t.end();
|
109
|
+
});
|
110
|
+
|
111
|
+
t.test('when `rawMetadata` is not a valid XML', async (t) => {
|
112
|
+
const body = Object.assign({}, config[0]);
|
113
|
+
body['rawMetadata'] = 'not a valid XML';
|
114
|
+
|
115
|
+
try {
|
116
|
+
await apiController.config(body);
|
117
|
+
t.fail('Expecting Error.');
|
118
|
+
} catch (err) {
|
119
|
+
t.match(err.message, /Non-whitespace before first tag./);
|
120
|
+
}
|
121
|
+
|
122
|
+
t.end();
|
123
|
+
});
|
124
|
+
});
|
125
|
+
|
126
|
+
t.test('when the request is good', async (t) => {
|
127
|
+
const body = Object.assign({}, config[0]);
|
128
|
+
const CLIENTID = '75edb050796a0eb1cf2cfb0da7245f85bc50baa7';
|
129
|
+
const PROVIDER = 'accounts.google.com';
|
130
|
+
|
131
|
+
const kdStub = sinon.stub(dbutils, 'keyDigest').returns(CLIENTID);
|
132
|
+
const rbStub = sinon
|
133
|
+
.stub(crypto, 'randomBytes')
|
134
|
+
.returns('f3b0f91eb8f4a9f7cc2254e08682d50b05b5d36262929e7f');
|
135
|
+
|
136
|
+
const response = await apiController.config(body);
|
137
|
+
t.ok(kdStub.called);
|
138
|
+
t.ok(rbStub.calledOnce);
|
139
|
+
t.equal(response.client_id, CLIENTID);
|
140
|
+
t.equal(
|
141
|
+
response.client_secret,
|
142
|
+
'f3b0f91eb8f4a9f7cc2254e08682d50b05b5d36262929e7f'
|
143
|
+
);
|
144
|
+
t.equal(response.provider, PROVIDER);
|
145
|
+
|
146
|
+
const savedConf = await apiController.getConfig({
|
147
|
+
clientID: CLIENTID,
|
148
|
+
});
|
149
|
+
t.equal(savedConf.provider, PROVIDER);
|
150
|
+
|
151
|
+
dbutils.keyDigest.restore();
|
152
|
+
crypto.randomBytes.restore();
|
153
|
+
|
154
|
+
t.end();
|
155
|
+
});
|
156
|
+
|
157
|
+
t.end();
|
158
|
+
});
|
159
|
+
|
160
|
+
t.end();
|
161
|
+
});
|