@boxyhq/saml-jackson 0.1.3 → 0.1.5-beta.101

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
@@ -7,12 +7,30 @@ You need someone like Jules Winnfield to save you from the vagaries of SAML logi
7
7
  ## Source code visualizer
8
8
  [CodeSee codebase visualizer](https://app.codesee.io/maps/public/53e91640-23b5-11ec-a724-79d7dd589517)
9
9
 
10
- # npm library
11
- Jackson is available as an npm library (https://www.npmjs.com/package/@boxyhq/saml-jackson) that can be integrated into express.js routes. In theory, the library should be usable with other node.js web application frameworks but is currently untested. Please file an issue or submit a PR if you encounter any issues.
10
+ ## Getting Started
12
11
 
13
- Refer to https://github.com/boxyhq/jackson#configuration for the configuration options to pass to the library.
12
+ There are two ways to use this repo.
13
+ - As an npm library
14
+ - As a separate service
15
+
16
+ ### Install as an npm library
17
+ Jackson is available as an [npm package](https://www.npmjs.com/package/@boxyhq/saml-jackson) that can be integrated into Express.js routes. library should be usable with other node.js web application frameworks but is currently untested. Please file an issue or submit a PR if you encounter any issues.
18
+
19
+ ```
20
+ npm i @boxyhq/saml-jackson
21
+ ```
22
+
23
+ ### Docker
24
+ The docker container can be found at [boxyhq/jackson](https://hub.docker.com/r/boxyhq/jackson/tags). It is preferable to use a specific version instead of the `latest` tag. Jackson uses two ports (configurable if needed, see below) 5000 and 6000. 6000 is the internal port and ideally should not be exposed to a public network.
25
+
26
+ ```
27
+ docker run -p 5000:5000 -p 6000:6000 boxyhq/jackson:78e9099d
28
+ ```
29
+
30
+ ### Usage
31
+
32
+ #### 1. Add Express Routes
14
33
 
15
- Here's how to use the npm library:
16
34
  ```
17
35
  // express
18
36
  const express = require('express');
@@ -94,66 +112,17 @@ router.get('/oauth/userinfo', cors(), async (req, res) => {
94
112
  app.user('/sso', router);
95
113
 
96
114
  ```
97
- You can also refer to our usage of the library internally in the Jackson service here - https://github.com/boxyhq/jackson/blob/main/src/jackson.js
98
-
99
- # Deployment
100
- The docker container can be found at https://hub.docker.com/r/boxyhq/jackson/tags. It is preferable to use a specific version instead of the `latest` tag. Jackson uses two ports (configurable if needed, see below) 5000 and 6000. 6000 is the internal port and ideally should not be exposed to a public network.
101
-
102
- Example of a docker run:
103
- ```
104
- docker run -p 5000:5000 -p 6000:6000 boxyhq/jackson:78e9099d
105
- ```
106
-
107
- # Database Support
108
- Jackson currently supports SQL databases (Postgres, CockroachDB, MySQL and MariaDB), MongoDB and Redis.
109
-
110
- # Configuration
111
- Configuration is done via env vars (and in the case of the npm library via an options object). The following options are supported and will have to be configured during deployment:
112
- - HOST_URL: The URL to bind to, defaults to `localhost`
113
- - HOST_PORT: The port to bind to, defaults to `5000`
114
- - EXTERNAL_URL (npm: externalUrl): The public URL to reach this service, used internally for documenting the SAML configuration instructions. Defaults to `http://{HOST_URL}:{HOST_PORT}` for Jackson service, required for npm library
115
- - INTERNAL_HOST_URL: The URL to bind to expose the internal APIs, defaults to `localhost`. Do not configure this to a public network
116
- - INTERNAL_HOST_PORT: The port to bind to for the internal APIs, defaults to `6000`
117
- - SAML_AUDIENCE (npm: samlAudience): This is just an identitifer 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. Defaults to `https://saml.boxyhq.com` and is case sensitive. This does not have be a real URL
118
- - 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. Defaults to `false`
119
- - DB_ENGINE (npm: db.engine): Supported values are `redis`, `sql`, `mongo`, `mem`. Defaults to `sql`
120
- - DB_URL (npm: db.url): The database URL to connect to, for example `postgres://postgres:postgres@localhost:5450/jackson`
121
- - DB_TYPE (npm: db.type): Only needed when DB_ENGINE is `sql`. Supported values are `postgres`, `cockroachdb`, `mysql`, `mariadb`. Defaults to `postgres`
122
- - 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.
123
-
124
- # Pre-loaded SAML Configuration
125
- If PRE_LOADED_CONFIG is set then it should point to a directory with the following structure (example below):-
126
- ```
127
- boxyhq.js
128
- boxyhq.xml
129
- anothertenant.js
130
- anothertenant.xml
131
- ```
132
- The JS file has the following structure:-
133
- ```
134
- module.exports = {
135
- defaultRedirectUrl: 'http://localhost:3000/login/saml',
136
- redirectUrl: '["http://localhost:3000/*", "http://localhost:5000/*"]',
137
- tenant: 'boxyhq.com',
138
- product: 'demo',
139
- };
140
- ```
141
- The XML file (should share the name with the .js file) is the raw XML metadata file you receive from your Identity Provider. Please ensure it is saved in the `utf-8` encoding.
142
115
 
143
- The config and XML above correspond to the `SAML API config` (see below).
144
-
145
- # SAML Login flows
146
- There are two kinds of SAML login flows - SP-initiated and IdP-initiated. We highly recommend sticking to the SP-initiated flow since it is more secure but Jackson also supports the IdP-initiated flow if you enable it. For an in-depth understanding of SAML and the two flows please refer to Okta's comprehensive guide - https://developer.okta.com/docs/concepts/saml/.
147
-
148
- # Setting up SAML with your customer's Identity Provider
116
+ #### 2. Setting up SAML with your customer's Identity Provider
149
117
  Please follow the instructions here to guide your customer's in setting up SAML correctly for your product(s). You should create a copy of the doc and modify it with your custom settings, we have used the values that work for our demo apps - https://docs.google.com/document/d/1fk---Z9Ln59u-2toGKUkyO3BF6Dh3dscT2u4J2xHANE.
150
118
 
151
- # SAML config API
119
+ #### 3. SAML config API
152
120
  Once your customer has set up the SAML app on their Identity Provider, the Identity Provider will generate an IdP or SP metadata file. Some Identity Providers only generate an IdP metadata file but it usually works for the SP login flow as well. It is an XML file that contains various attributes Jackson needs in order to validate incoming SAML login requests. This step is the equivalent of setting an OAuth 2.0 app and generating a client ID and client secret that will be used in the login flow.
153
121
 
154
122
  You will need to provide a place in the UI for your customers (The account settings page is usually a good place for this) to configure this and then call the API below.
155
123
 
156
124
  The following API call sets up the configuration in Jackson:
125
+
157
126
  ```
158
127
  curl --location --request POST 'http://localhost:6000/api/v1/saml/config' \
159
128
  --header 'Content-Type: application/x-www-form-urlencoded' \
@@ -167,18 +136,19 @@ curl --location --request POST 'http://localhost:6000/api/v1/saml/config' \
167
136
  - rawMetadata: The XML metadata file your customer gets from their Identity Provider
168
137
  - defaultRedirectUrl: The redirect URL to use in the IdP login flow. Jackson will call this URL after completing an IdP login flow
169
138
  - redirectUrl: JSON encoded array containing a list of allowed redirect URLs. Jackson will disallow any redirects not on this list (or not the default URL above)
170
- - tenant: Jackson supports a multi-tenant architecture, this is a unique identifier you set from your side that relates back to your customer's tenant. This is normally an email, domain, an account id, or user id
139
+ - tenant: Jackson supports a multi-tenant architecture, this is a unique identifier you set from your side that relates back to your customer's tenant. This is normally an email, domain, an account id, or user-id
171
140
  - product: Jackson support multiple products, this is a unique identifier you set from your side that relates back to the product your customer is using
172
141
 
173
142
  The response returns a JSON with `client_id` and `client_secret` that can be stored against your tenant and product for a more secure OAuth 2.0 flow. If you do not want to store the `client_id` and `client_secret` you can alternatively use `client_id=tentant=<tenantID>&product=<productID>` and any arbitrary value for `client_secret` when setting up the OAuth 2.0 flow.
174
143
 
175
- # OAuth 2.0 Flow
144
+ #### 4. OAuth 2.0 Flow
176
145
  Jackson has been designed to abstract the SAML login flow as a pure OAuth 2.0 flow. This means it's compatible with any standard OAuth 2.0 library out there, both client-side and server-side. It is important to remember that SAML is configured per customer unlike OAuth 2.0 where you can have a single OAuth app supporting logins for all customers.
177
146
 
178
147
  Jackson also supports the PKCE authorization flow (https://oauth.net/2/pkce/), so you can protect your SPAs.
179
148
 
180
149
  If for any reason you need to implement the flow on your own, the steps are outlined below:
181
- ## Authorize
150
+
151
+ #### 5. Authorize
182
152
  The OAuth flow begins with redirecting your user to the `authorize` URL:
183
153
  ```
184
154
  https://localhost:5000/oauth/authorize
@@ -193,7 +163,7 @@ https://localhost:5000/oauth/authorize
193
163
  - redirect_uri: This is where the user will be taken back once the authorization flow is complete
194
164
  - 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
195
165
 
196
- ## Code exchange
166
+ #### 6. Code Exchange
197
167
  After successful authorization, the user is redirected back to the `redirect_uri`. The query parameters will include the `code` and `state` parameters. You should validate that the state matches the one you sent in the authorize request.
198
168
 
199
169
  The code can then be exchanged for a token by making the following request:
@@ -221,7 +191,7 @@ If everything goes well you should receive a JSON response that includes the acc
221
191
  }
222
192
  ```
223
193
 
224
- ## Profile request
194
+ #### 7. Profile Request
225
195
  The short-lived access token can now be used to request the user's profile. You'll need to make the following request:
226
196
  ```
227
197
  curl --request GET \
@@ -244,3 +214,78 @@ If everything goes well you should receive a JSON response with the user's profi
244
214
  - id: The id of the user as provided by the Identity Provider
245
215
  - firstName: The first name of the user as provided by the Identity Provider
246
216
  - lastName: The last name of the user as provided by the Identity Provider
217
+
218
+ ## Examples
219
+ To Do
220
+
221
+ ## Database Support
222
+ Jackson currently supports the following databases.
223
+
224
+ - Postgres
225
+ - CockroachDB
226
+ - MySQL
227
+ - MariaDB
228
+ - MongoDB
229
+ - Redis
230
+
231
+ ## Configuration
232
+ Configuration is done via env vars (and in the case of the npm library via an options object).
233
+
234
+ The following options are supported and will have to be configured during deployment.
235
+
236
+ | Key | Description | Default |
237
+ |-----------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------|
238
+ | HOST_URL | The URL to bind to | `localhost` |
239
+ | HOST_PORT | The port to bind to | `5000` |
240
+ | EXTERNAL_URL (npm: externalUrl) | The public URL to reach this service, used internally for documenting the SAML configuration instructions. | `http://{HOST_URL}:{HOST_PORT}` |
241
+ | INTERNAL_HOST_URL | The URL to bind to expose the internal APIs. Do not configure this to a public network. | `localhost` |
242
+ | INTERNAL_HOST_PORT | The port to bind to for the internal APIs. | `6000` |
243
+ | 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` |
244
+ | 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` |
245
+ | DB_ENGINE (npm: db.engine) | Supported values are `redis`, `sql`, `mongo`, `mem`. | `sql` |
246
+ | DB_URL (npm: db.url) | The database URL to connect to. For example `postgres://postgres:postgres@localhost:5450/jackson` | |
247
+ | DB_TYPE (npm: db.type) | Only needed when DB_ENGINE is `sql`. Supported values are `postgres`, `cockroachdb`, `mysql`, `mariadb`. | `postgres` |
248
+ | 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. | |
249
+
250
+ ## Pre-loaded SAML Configuration
251
+ If PRE_LOADED_CONFIG is set then it should point to a directory with the following structure (example below):-
252
+ ```
253
+ boxyhq.js
254
+ boxyhq.xml
255
+ anothertenant.js
256
+ anothertenant.xml
257
+ ```
258
+ The JS file has the following structure:-
259
+ ```
260
+ module.exports = {
261
+ defaultRedirectUrl: 'http://localhost:3000/login/saml',
262
+ redirectUrl: '["http://localhost:3000/*", "http://localhost:5000/*"]',
263
+ tenant: 'boxyhq.com',
264
+ product: 'demo',
265
+ };
266
+ ```
267
+ The XML file (should share the name with the .js file) is the raw XML metadata file you receive from your Identity Provider. Please ensure it is saved in the `utf-8` encoding.
268
+
269
+ The config and XML above correspond to the `SAML API config` (see below).
270
+
271
+ ## SAML Login flows
272
+ There are two kinds of SAML login flows - SP-initiated and IdP-initiated. We highly recommend sticking to the SP-initiated flow since it is more secure but Jackson also supports the IdP-initiated flow if you enable it. For an in-depth understanding of SAML and the two flows please refer to Okta's comprehensive guide - https://developer.okta.com/docs/concepts/saml/.
273
+
274
+ ## Contributing
275
+ Thanks for taking the time to contribute! Contributions are what makes the open-source community such an amazing place to learn, inspire, and create. Any contributions you make will benefit everybody else and are appreciated.
276
+
277
+ Please try to create bug reports that are:
278
+
279
+ - _Reproducible._ Include steps to reproduce the problem.
280
+ - _Specific._ Include as much detail as possible: which version, what environment, etc.
281
+ - _Unique._ Do not duplicate existing opened issues.
282
+ - _Scoped to a Single Bug._ One bug per report.
283
+
284
+ ## Support
285
+ Reach out to the maintainer at one of the following places:
286
+
287
+ - [GitHub Issues](https://github.com/boxyhq/jackson/issues)
288
+ - The email which is located [in GitHub profile](https://github.com/deepakprabhakara)
289
+
290
+ ## License
291
+ [Apache 2.0 License](https://github.com/boxyhq/jackson/blob/main/LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxyhq/saml-jackson",
3
- "version": "0.1.3",
3
+ "version": "0.1.5-beta.101",
4
4
  "license": "Apache 2.0",
5
5
  "description": "SAML 2.0 service",
6
6
  "main": "src/index.js",
@@ -20,7 +20,7 @@
20
20
  "calendso": "cross-env DB_URL=postgresql://postgres:postgres@localhost:5450/calendso nodemon src/jackson.js",
21
21
  "mongo": "cross-env DB_ENGINE=mongo DB_URL=mongodb://localhost:27017/jackson nodemon src/jackson.js",
22
22
  "pre-loaded": "cross-env DB_ENGINE=mem PRE_LOADED_CONFIG='./_config' nodemon src/jackson.js",
23
- "test": "tap src/**/*.test.js",
23
+ "test": "tap --timeout=100 src/**/*.test.js",
24
24
  "dev-dbs": "docker-compose -f ./_dev/docker-compose.yml up -d"
25
25
  },
26
26
  "tap": {
@@ -32,27 +32,27 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@boxyhq/saml20": "0.2.0",
35
- "@peculiar/webcrypto": "1.1.7",
36
- "@peculiar/x509": "1.4.1",
35
+ "@peculiar/webcrypto": "1.2.2",
36
+ "@peculiar/x509": "1.6.0",
37
37
  "cors": "2.8.5",
38
38
  "express": "4.17.1",
39
- "mongodb": "4.1.3",
40
- "mysql2": "^2.3.3-rc.0",
39
+ "mongodb": "4.2.0",
40
+ "mysql2": "2.3.3",
41
41
  "pg": "8.7.1",
42
42
  "rambda": "6.9.0",
43
- "redis": "4.0.0-rc.3",
43
+ "redis": "4.0.0-rc.4",
44
44
  "reflect-metadata": "0.1.13",
45
45
  "ripemd160": "2.0.2",
46
46
  "thumbprint": "0.0.1",
47
- "typeorm": "0.2.38",
47
+ "typeorm": "0.2.41",
48
48
  "xml-crypto": "2.1.3",
49
49
  "xml2js": "0.4.23",
50
50
  "xmlbuilder": "15.1.1"
51
51
  },
52
52
  "devDependencies": {
53
53
  "cross-env": "7.0.3",
54
- "eslint": "^8.2.0",
55
- "nodemon": "2.0.13",
56
- "tap": "15.0.10"
54
+ "eslint": "8.3.0",
55
+ "nodemon": "2.0.15",
56
+ "tap": "15.1.2"
57
57
  }
58
58
  }
@@ -28,10 +28,16 @@ const extractBearerToken = (req) => {
28
28
  function getEncodedClientId(client_id) {
29
29
  try {
30
30
  const sp = new URLSearchParams(client_id);
31
- return {
32
- tenant: sp.get('tenant'),
33
- product: sp.get('product'),
34
- };
31
+ const tenant = sp.get('tenant');
32
+ const product = sp.get('product');
33
+ if (tenant && product) {
34
+ return {
35
+ tenant: sp.get('tenant'),
36
+ product: sp.get('product'),
37
+ };
38
+ }
39
+
40
+ return null;
35
41
  } catch (err) {
36
42
  return null;
37
43
  }
@@ -287,7 +293,7 @@ const token = async (req, res) => {
287
293
  res.json({
288
294
  access_token: token,
289
295
  token_type: 'bearer',
290
- expires_in: 300,
296
+ expires_in: options.db.ttl,
291
297
  });
292
298
  };
293
299
 
package/src/db/db.js CHANGED
@@ -19,7 +19,7 @@ class DB {
19
19
 
20
20
  // ttl is in seconds
21
21
  async put(namespace, key, val, ttl = 0, ...indexes) {
22
- if (this.ttl > 0 && indexes && indexes.length > 0) {
22
+ if (ttl > 0 && indexes && indexes.length > 0) {
23
23
  throw new Error('secondary indexes not allow on a store with ttl');
24
24
  }
25
25
 
package/src/db/db.test.js CHANGED
@@ -3,6 +3,8 @@ const t = require('tap');
3
3
  const DB = require('./db.js');
4
4
 
5
5
  let configStores = [];
6
+ let ttlStores = [];
7
+ const ttl = 3;
6
8
 
7
9
  const record1 = {
8
10
  id: '1',
@@ -18,48 +20,46 @@ const record2 = {
18
20
  const dbs = [
19
21
  {
20
22
  engine: 'mem',
21
- options: {},
23
+ ttl: 1,
22
24
  },
23
25
  {
24
26
  engine: 'redis',
25
- options: { url: 'redis://localhost:6379' },
27
+ url: 'redis://localhost:6379',
26
28
  },
27
29
  {
28
30
  engine: 'sql',
29
- options: {
30
- url: 'postgresql://postgres:postgres@localhost:5432/postgres',
31
- type: 'postgres',
32
- },
31
+ url: 'postgresql://postgres:postgres@localhost:5432/postgres',
32
+ type: 'postgres',
33
+ ttl: 1,
34
+ limit: 1,
33
35
  },
34
36
  {
35
37
  engine: 'mongo',
36
- options: { url: 'mongodb://localhost:27017/jackson' },
38
+ url: 'mongodb://localhost:27017/jackson',
37
39
  },
38
40
  {
39
41
  engine: 'sql',
40
- options: {
41
- url: 'mysql://root:mysql@localhost:3307/mysql',
42
- type: 'mysql',
43
- },
42
+ url: 'mysql://root:mysql@localhost:3307/mysql',
43
+ type: 'mysql',
44
+ ttl: 1,
45
+ limit: 1,
44
46
  },
45
47
  {
46
48
  engine: 'sql',
47
- options: {
48
- url: 'mariadb://root@localhost:3306/mysql',
49
- type: 'mariadb',
50
- },
49
+ url: 'mariadb://root@localhost:3306/mysql',
50
+ type: 'mariadb',
51
+ ttl: 1,
52
+ limit: 1,
51
53
  },
52
54
  ];
53
55
 
54
56
  t.before(async () => {
55
57
  for (const idx in dbs) {
56
- const config = dbs[idx];
57
- const engine = config.engine;
58
- const opts = config.options;
59
- opts.engine = engine;
58
+ const opts = dbs[idx];
60
59
  const db = await DB.new(opts);
61
60
 
62
61
  configStores.push(db.store('saml:config'));
62
+ ttlStores.push(db.store('oauth:session', ttl));
63
63
  }
64
64
  });
65
65
 
@@ -70,9 +70,10 @@ t.teardown(async () => {
70
70
  t.test('dbs', ({ end }) => {
71
71
  for (const idx in configStores) {
72
72
  const configStore = configStores[idx];
73
+ const ttlStore = ttlStores[idx];
73
74
  let dbEngine = dbs[idx].engine;
74
- if (dbs[idx].options.type) {
75
- dbEngine += ': ' + dbs[idx].options.type;
75
+ if (dbs[idx].type) {
76
+ dbEngine += ': ' + dbs[idx].type;
76
77
  }
77
78
  t.test('put(): ' + dbEngine, async (t) => {
78
79
  await configStore.put(
@@ -109,8 +110,8 @@ t.test('dbs', ({ end }) => {
109
110
  });
110
111
 
111
112
  t.test('get(): ' + dbEngine, async (t) => {
112
- const ret1 = await configStore.get('1');
113
- const ret2 = await configStore.get('2');
113
+ const ret1 = await configStore.get(record1.id);
114
+ const ret2 = await configStore.get(record2.id);
114
115
 
115
116
  t.same(ret1, record1, 'unable to get record1');
116
117
  t.same(ret2, record2, 'unable to get record2');
@@ -151,8 +152,8 @@ t.test('dbs', ({ end }) => {
151
152
 
152
153
  await configStore.delete(record2.id);
153
154
 
154
- const ret1 = await configStore.get('1');
155
- const ret2 = await configStore.get('2');
155
+ const ret1 = await configStore.get(record1.id);
156
+ const ret2 = await configStore.get(record2.id);
156
157
 
157
158
  const ret3 = await configStore.getByIndex({
158
159
  name: 'name',
@@ -171,6 +172,69 @@ t.test('dbs', ({ end }) => {
171
172
 
172
173
  t.end();
173
174
  });
175
+
176
+ t.test('ttl indexes: ' + dbEngine, async (t) => {
177
+ try {
178
+ await ttlStore.put(
179
+ record1.id,
180
+ record1,
181
+ {
182
+ // secondary index on city
183
+ name: 'city',
184
+ value: record1.city,
185
+ },
186
+ {
187
+ // secondary index on name
188
+ name: 'name',
189
+ value: record1.name,
190
+ }
191
+ );
192
+
193
+ t.fail('expecting a secondary indexes not allow on a store with ttl');
194
+ } catch (err) {
195
+ t.ok(err, 'got expected error');
196
+ }
197
+
198
+ t.end();
199
+ });
200
+
201
+ t.test('ttl put(): ' + dbEngine, async (t) => {
202
+ await ttlStore.put(record1.id, record1);
203
+
204
+ await ttlStore.put(record2.id, record2);
205
+
206
+ t.end();
207
+ });
208
+
209
+ t.test('ttl get(): ' + dbEngine, async (t) => {
210
+ const ret1 = await ttlStore.get(record1.id);
211
+ const ret2 = await ttlStore.get(record2.id);
212
+
213
+ t.same(ret1, record1, 'unable to get record1');
214
+ t.same(ret2, record2, 'unable to get record2');
215
+
216
+ t.end();
217
+ });
218
+
219
+ t.test('ttl expiry: ' + dbEngine, async (t) => {
220
+ // mongo runs ttl task every 60 seconds
221
+ if (dbEngine.startsWith('mongo')) {
222
+ t.end();
223
+ return;
224
+ }
225
+
226
+ await new Promise((resolve) =>
227
+ setTimeout(resolve, ((dbEngine === 'mem' ? 5 : 0) + ttl + 0.5) * 1000)
228
+ );
229
+
230
+ const ret1 = await ttlStore.get(record1.id);
231
+ const ret2 = await ttlStore.get(record2.id);
232
+
233
+ t.same(ret1, null, 'ttl for record1 failed');
234
+ t.same(ret2, null, 'ttl for record2 failed');
235
+
236
+ t.end();
237
+ });
174
238
  }
175
239
 
176
240
  t.test('db.new() error', async (t) => {
package/src/db/mem.js CHANGED
@@ -2,14 +2,32 @@
2
2
  const dbutils = require('./utils.js');
3
3
 
4
4
  class Mem {
5
- constructor(/*options*/) {
5
+ constructor(options) {
6
6
  return (async () => {
7
7
  this.store = {}; // map of key, value
8
8
  this.indexes = {}; // map of key, Set
9
9
  this.cleanup = {}; // map of indexes for cleanup when store key is deleted
10
10
  this.ttlStore = {}; // map of key to ttl
11
11
 
12
- return this; // Return the newly-created instance
12
+ if (options.ttl) {
13
+ this.ttlCleanup = async () => {
14
+ const now = Date.now();
15
+ for (const k in this.ttlStore) {
16
+ if (this.ttlStore[k].expiresAt < now) {
17
+ await this.delete(
18
+ this.ttlStore[k].namespace,
19
+ this.ttlStore[k].key
20
+ );
21
+ }
22
+ }
23
+
24
+ this.timerId = setTimeout(this.ttlCleanup, options.ttl * 1000);
25
+ };
26
+
27
+ this.timerId = setTimeout(this.ttlCleanup, options.ttl * 1000);
28
+ }
29
+
30
+ return this;
13
31
  })();
14
32
  }
15
33
 
@@ -39,8 +57,11 @@ class Mem {
39
57
  this.store[k] = JSON.stringify(val);
40
58
 
41
59
  if (ttl) {
42
- // TODO: implement cleanup of TTL
43
- this.ttlStore[k] = Date.now() + ttl * 1000;
60
+ this.ttlStore[k] = {
61
+ namespace,
62
+ key,
63
+ expiresAt: Date.now() + ttl * 1000,
64
+ };
44
65
  }
45
66
 
46
67
  // no ttl support for secondary indexes
@@ -79,6 +100,7 @@ class Mem {
79
100
  }
80
101
 
81
102
  delete this.cleanup[idxKey];
103
+ delete this.ttlStore[k];
82
104
  }
83
105
  }
84
106
 
@@ -13,9 +13,11 @@ module.exports = new EntitySchema({
13
13
  },
14
14
  key: {
15
15
  type: 'varchar',
16
+ length: 1500,
16
17
  },
17
18
  storeKey: {
18
19
  type: 'varchar',
20
+ length: 1500,
19
21
  }
20
22
  },
21
23
  relations: {
@@ -1,20 +1,36 @@
1
1
  const EntitySchema = require('typeorm').EntitySchema;
2
2
  const JacksonStore = require('../model/JacksonStore.js');
3
3
 
4
- module.exports = new EntitySchema({
5
- name: 'JacksonStore',
6
- target: JacksonStore,
7
- columns: {
8
- key: {
9
- primary: true,
10
- type: 'varchar',
11
- },
12
- value: {
13
- type: 'varchar',
4
+ const valueType = (type) => {
5
+ switch (type) {
6
+ case 'postgres':
7
+ case 'cockroachdb':
8
+ return 'text';
9
+ case 'mysql':
10
+ case 'mariadb':
11
+ return 'mediumtext';
12
+ default:
13
+ return 'varchar';
14
+ }
15
+ };
16
+
17
+ module.exports = (type) => {
18
+ return new EntitySchema({
19
+ name: 'JacksonStore',
20
+ target: JacksonStore,
21
+ columns: {
22
+ key: {
23
+ primary: true,
24
+ type: 'varchar',
25
+ length: 1500,
26
+ },
27
+ value: {
28
+ type: valueType(type),
29
+ },
30
+ expiresAt: {
31
+ type: 'bigint',
32
+ nullable: true,
33
+ },
14
34
  },
15
- expiresAt: {
16
- type: 'bigint',
17
- nullable: true,
18
- }
19
- },
20
- });
35
+ });
36
+ };
package/src/db/sql/sql.js CHANGED
@@ -8,22 +8,60 @@ const dbutils = require('../utils.js');
8
8
  class Sql {
9
9
  constructor(options) {
10
10
  return (async () => {
11
- this.connection = await typeorm.createConnection({
12
- name: options.type,
13
- type: options.type,
14
- url: options.url,
15
- synchronize: true,
16
- logging: false,
17
- entities: [
18
- require('./entity/JacksonStore.js'),
19
- require('./entity/JacksonIndex.js'),
20
- ],
21
- });
11
+ while (true) {
12
+ try {
13
+ this.connection = await typeorm.createConnection({
14
+ name: options.type,
15
+ type: options.type,
16
+ url: options.url,
17
+ synchronize: true,
18
+ migrationsTableName: '_jackson_migrations',
19
+ logging: false,
20
+ entities: [
21
+ require('./entity/JacksonStore.js')(options.type),
22
+ require('./entity/JacksonIndex.js'),
23
+ ],
24
+ });
25
+
26
+ break;
27
+ } catch (err) {
28
+ console.error(`error connecting to ${options.type} db: ${err}`);
29
+ await dbutils.sleep(1000);
30
+ continue;
31
+ }
32
+ }
22
33
 
23
34
  this.storeRepository = this.connection.getRepository(JacksonStore);
24
35
  this.indexRepository = this.connection.getRepository(JacksonIndex);
25
36
 
26
- return this; // Return the newly-created instance
37
+ if (options.ttl && options.limit) {
38
+ this.ttlCleanup = async () => {
39
+ const now = Date.now();
40
+
41
+ while (true) {
42
+ const ids = await this.storeRepository.find({
43
+ expiresAt: typeorm.MoreThan(now),
44
+ take: options.limit,
45
+ });
46
+
47
+ if (ids.length <= 0) {
48
+ break;
49
+ }
50
+
51
+ await this.storeRepository.remove(ids);
52
+ }
53
+
54
+ this.timerId = setTimeout(this.ttlCleanup, options.ttl * 1000);
55
+ };
56
+
57
+ this.timerId = setTimeout(this.ttlCleanup, options.ttl * 1000);
58
+ } else {
59
+ console.log(
60
+ 'Warning: ttl cleanup not enabled, set both "ttl" and "limit" options to enable it!'
61
+ );
62
+ }
63
+
64
+ return this;
27
65
  })();
28
66
  }
29
67
 
package/src/db/utils.js CHANGED
@@ -16,14 +16,15 @@ const keyFromParts = (...parts) => {
16
16
  return parts.join(':'); // TODO: pick a better strategy, keys can collide now
17
17
  };
18
18
 
19
+ const sleep = (ms) => {
20
+ return new Promise((resolve) => setTimeout(resolve, ms));
21
+ };
22
+
19
23
  module.exports = {
20
24
  key,
21
-
22
25
  keyForIndex,
23
-
24
26
  keyDigest,
25
-
26
27
  keyFromParts,
27
-
28
+ sleep,
28
29
  indexPrefix: '_index',
29
30
  };
package/src/env.js CHANGED
@@ -15,6 +15,7 @@ const db = {
15
15
  engine: process.env.DB_ENGINE,
16
16
  url: process.env.DB_URL,
17
17
  type: process.env.DB_TYPE,
18
+ ttl: process.env.DB_TTL,
18
19
  };
19
20
 
20
21
  module.exports = {
package/src/index.js CHANGED
@@ -21,6 +21,8 @@ const defaultOpts = (opts) => {
21
21
  newOpts.db.url =
22
22
  newOpts.db.url || 'postgres://postgres:postgres@localhost:5432/jackson';
23
23
  newOpts.db.type = newOpts.db.type || 'postgres'; // Only needed if DB_ENGINE is sql. Supported values: postgres, cockroachdb, mysql, mariadb
24
+ newOpts.db.ttl = (newOpts.db.ttl || 300) * 1; // TTL for the code, session and token stores (in seconds)
25
+ newOpts.db.limit = (newOpts.db.limit || 1000) * 1; // Limit ttl cleanup to this many items at a time
24
26
 
25
27
  return newOpts;
26
28
  };
@@ -30,9 +32,9 @@ module.exports = async function (opts) {
30
32
 
31
33
  const db = await DB.new(opts.db);
32
34
  const configStore = db.store('saml:config');
33
- const sessionStore = db.store('oauth:session', 300);
34
- const codeStore = db.store('oauth:code', 300);
35
- const tokenStore = db.store('oauth:token', 300);
35
+ const sessionStore = db.store('oauth:session', opts.db.ttl);
36
+ const codeStore = db.store('oauth:code', opts.db.ttl);
37
+ const tokenStore = db.store('oauth:token', opts.db.ttl);
36
38
 
37
39
  const apiController = require('./controller/api.js')({ configStore });
38
40
  const oauthController = require('./controller/oauth.js')({
@@ -54,7 +56,8 @@ module.exports = async function (opts) {
54
56
  }
55
57
  }
56
58
 
57
- console.log(`Using engine: ${opts.db.engine}`);
59
+ const type = opts.db.type ? ' Type: ' + opts.db.type : '';
60
+ console.log(`Using engine: ${opts.db.engine}.${type}`);
58
61
 
59
62
  return {
60
63
  apiController,
package/src/jackson.js CHANGED
@@ -78,7 +78,7 @@ internalApp.post(apiPath + '/config', async (req, res) => {
78
78
 
79
79
  let internalServer = server;
80
80
  if (env.useInternalServer) {
81
- internalServer = internalApp.listen(env.internalPort, async () => {
81
+ internalServer = internalApp.listen(env.internalHostPort, async () => {
82
82
  console.log(
83
83
  `🚀 The path of the righteous internal server: http://${env.internalHostUrl}:${env.internalHostPort}`
84
84
  );