@boxyhq/saml-jackson 0.1.5-beta.101 → 0.1.5-beta.105
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 +81 -77
- package/package.json +1 -2
- package/src/controller/oauth.js +6 -1
- package/src/index.js +2 -2
package/README.md
CHANGED
@@ -1,43 +1,34 @@
|
|
1
1
|
# SAML Jackson (not fiction anymore)
|
2
|
-
|
2
|
+
|
3
3
|
SAML service [SAML in a box from BoxyHQ]
|
4
|
-
|
4
|
+
|
5
5
|
You need someone like Jules Winnfield to save you from the vagaries of SAML login.
|
6
|
-
|
7
|
-
|
6
|
+
|
7
|
+
# Source code visualizer
|
8
8
|
[CodeSee codebase visualizer](https://app.codesee.io/maps/public/53e91640-23b5-11ec-a724-79d7dd589517)
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
|
10
|
+
# Getting Started
|
11
|
+
|
12
12
|
There are two ways to use this repo.
|
13
|
-
- As an npm library
|
13
|
+
- As an npm library (for Express compatible frameworks)
|
14
14
|
- As a separate service
|
15
|
-
|
16
|
-
|
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
|
-
|
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. 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.
|
18
|
+
|
19
19
|
```
|
20
20
|
npm i @boxyhq/saml-jackson
|
21
21
|
```
|
22
|
-
|
23
|
-
###
|
24
|
-
|
25
|
-
|
26
|
-
```
|
27
|
-
docker run -p 5000:5000 -p 6000:6000 boxyhq/jackson:78e9099d
|
28
|
-
```
|
29
|
-
|
30
|
-
### Usage
|
31
|
-
|
32
|
-
#### 1. Add Express Routes
|
33
|
-
|
22
|
+
|
23
|
+
### Add Express Routes
|
24
|
+
|
34
25
|
```
|
35
26
|
// express
|
36
27
|
const express = require('express');
|
37
28
|
const router = express.Router();
|
38
29
|
const cors = require('cors'); // needed if you are calling the token userinfo endpoints from the frontend
|
39
|
-
|
40
|
-
// Set the required options
|
30
|
+
|
31
|
+
// Set the required options. Refer to https://github.com/boxyhq/jackson#configuration for the full list
|
41
32
|
const opts = {
|
42
33
|
externalUrl: 'https://my-cool-app.com',
|
43
34
|
samlAudience: 'https://my-cool-app.com',
|
@@ -47,7 +38,7 @@ const opts = {
|
|
47
38
|
url: 'mongodb://localhost:27017/my-cool-app',
|
48
39
|
}
|
49
40
|
};
|
50
|
-
|
41
|
+
|
51
42
|
// Please note that the initialization of @boxyhq/saml-jackson is async, you cannot run it at the top level
|
52
43
|
// Run this in a function where you initialise the express server.
|
53
44
|
async function init() {
|
@@ -55,17 +46,17 @@ async function init() {
|
|
55
46
|
const apiController = ret.apiController;
|
56
47
|
const oauthController = ret.oauthController;
|
57
48
|
}
|
58
|
-
|
49
|
+
|
59
50
|
// express.js middlewares needed to parse json and x-www-form-urlencoded
|
60
51
|
router.use(express.json());
|
61
52
|
router.use(express.urlencoded({ extended: true }));
|
62
|
-
|
53
|
+
|
63
54
|
// SAML config API. You should pass this route through your authentication checks, do not expose this on the public interface without proper authentication in place.
|
64
55
|
router.post('/api/v1/saml/config', async (req, res) => {
|
65
56
|
try {
|
66
57
|
// apply your authentication flow (or ensure this route has passed through your auth middleware)
|
67
58
|
...
|
68
|
-
|
59
|
+
|
69
60
|
// only when properly authenticated, call the config function
|
70
61
|
res.json(await apiController.config(req.body));
|
71
62
|
} catch (err) {
|
@@ -74,7 +65,7 @@ router.post('/api/v1/saml/config', async (req, res) => {
|
|
74
65
|
});
|
75
66
|
}
|
76
67
|
});
|
77
|
-
|
68
|
+
|
78
69
|
// OAuth 2.0 flow
|
79
70
|
router.get('/oauth/authorize', async (req, res) => {
|
80
71
|
try {
|
@@ -83,7 +74,7 @@ router.get('/oauth/authorize', async (req, res) => {
|
|
83
74
|
res.status(500).send(err.message);
|
84
75
|
}
|
85
76
|
});
|
86
|
-
|
77
|
+
|
87
78
|
router.post('/oauth/saml', async (req, res) => {
|
88
79
|
try {
|
89
80
|
await oauthController.samlResponse(req, res);
|
@@ -91,7 +82,7 @@ router.post('/oauth/saml', async (req, res) => {
|
|
91
82
|
res.status(500).send(err.message);
|
92
83
|
}
|
93
84
|
});
|
94
|
-
|
85
|
+
|
95
86
|
router.post('/oauth/token', cors(), async (req, res) => {
|
96
87
|
try {
|
97
88
|
await oauthController.token(req, res);
|
@@ -99,7 +90,7 @@ router.post('/oauth/token', cors(), async (req, res) => {
|
|
99
90
|
res.status(500).send(err.message);
|
100
91
|
}
|
101
92
|
});
|
102
|
-
|
93
|
+
|
103
94
|
router.get('/oauth/userinfo', cors(), async (req, res) => {
|
104
95
|
try {
|
105
96
|
await oauthController.userInfo(req, res);
|
@@ -107,22 +98,35 @@ router.get('/oauth/userinfo', cors(), async (req, res) => {
|
|
107
98
|
res.status(500).send(err.message);
|
108
99
|
}
|
109
100
|
});
|
110
|
-
|
101
|
+
|
111
102
|
// set the router
|
112
103
|
app.user('/sso', router);
|
113
|
-
|
104
|
+
|
105
|
+
```
|
106
|
+
|
107
|
+
## Deployment as a service: Docker
|
108
|
+
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.
|
109
|
+
|
110
|
+
```
|
111
|
+
docker run -p 5000:5000 -p 6000:6000 boxyhq/jackson:78e9099d
|
114
112
|
```
|
115
113
|
|
116
|
-
|
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.
|
114
|
+
Refer to https://github.com/boxyhq/jackson#configuration for the full configuration.
|
118
115
|
|
119
|
-
|
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.
|
116
|
+
Kubernetes and docker-compose deployment files will be coming soon.
|
121
117
|
|
118
|
+
## Usage
|
119
|
+
|
120
|
+
### 1. Setting up SAML with your customer's Identity Provider
|
121
|
+
Please follow the instructions [here](https://docs.google.com/document/d/1fk---Z9Ln59u-2toGKUkyO3BF6Dh3dscT2u4J2xHANE) to guide your customers 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.
|
122
|
+
|
123
|
+
### 2. SAML config API
|
124
|
+
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 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.
|
125
|
+
|
122
126
|
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.
|
123
|
-
|
127
|
+
|
124
128
|
The following API call sets up the configuration in Jackson:
|
125
|
-
|
129
|
+
|
126
130
|
```
|
127
131
|
curl --location --request POST 'http://localhost:6000/api/v1/saml/config' \
|
128
132
|
--header 'Content-Type: application/x-www-form-urlencoded' \
|
@@ -132,23 +136,23 @@ curl --location --request POST 'http://localhost:6000/api/v1/saml/config' \
|
|
132
136
|
--data-urlencode 'tenant=boxyhq.com' \
|
133
137
|
--data-urlencode 'product=demo'
|
134
138
|
```
|
135
|
-
|
139
|
+
|
136
140
|
- rawMetadata: The XML metadata file your customer gets from their Identity Provider
|
137
141
|
- defaultRedirectUrl: The redirect URL to use in the IdP login flow. Jackson will call this URL after completing an IdP login flow
|
138
142
|
- 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)
|
139
143
|
- 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
|
140
144
|
- 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
|
141
|
-
|
145
|
+
|
142
146
|
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.
|
143
|
-
|
144
|
-
|
147
|
+
|
148
|
+
### 3. OAuth 2.0 Flow
|
145
149
|
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.
|
146
|
-
|
150
|
+
|
147
151
|
Jackson also supports the PKCE authorization flow (https://oauth.net/2/pkce/), so you can protect your SPAs.
|
148
|
-
|
152
|
+
|
149
153
|
If for any reason you need to implement the flow on your own, the steps are outlined below:
|
150
|
-
|
151
|
-
|
154
|
+
|
155
|
+
### 4. Authorize
|
152
156
|
The OAuth flow begins with redirecting your user to the `authorize` URL:
|
153
157
|
```
|
154
158
|
https://localhost:5000/oauth/authorize
|
@@ -157,15 +161,15 @@ https://localhost:5000/oauth/authorize
|
|
157
161
|
&redirect_uri=<redirect URL>
|
158
162
|
&state=<randomly generated state id>
|
159
163
|
```
|
160
|
-
|
164
|
+
|
161
165
|
- response_type=code: This is the only supported type for now but maybe extended in the future
|
162
166
|
- client_id: Use the client_id returned by the SAML config API or use `tentant=<tenantID>&product=<productID>` to use the tenant and product IDs instead
|
163
167
|
- redirect_uri: This is where the user will be taken back once the authorization flow is complete
|
164
168
|
- 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
|
165
|
-
|
166
|
-
|
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.
|
168
|
-
|
169
|
+
|
170
|
+
### 5. Code Exchange
|
171
|
+
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.
|
172
|
+
|
169
173
|
The code can then be exchanged for a token by making the following request:
|
170
174
|
```
|
171
175
|
curl --request POST \
|
@@ -181,7 +185,7 @@ curl --request POST \
|
|
181
185
|
- client_id: Use the client_id returned by the SAML config API or use `tentant=<tenantID>&product=<productID>` to use the tenant and product IDs instead
|
182
186
|
- client_secret: Use the client_secret returned by the SAML config API or any arbitrary value if using the tenant and product in the clientID
|
183
187
|
- redirect_uri: This is where the user will be taken back once the authorization flow is complete. Use the same redirect_uri as the previous request
|
184
|
-
|
188
|
+
|
185
189
|
If everything goes well you should receive a JSON response that includes the access token. This token is needed for the next step where we fetch the user profile.
|
186
190
|
```
|
187
191
|
{
|
@@ -190,8 +194,8 @@ If everything goes well you should receive a JSON response that includes the acc
|
|
190
194
|
"expires_in": 300
|
191
195
|
}
|
192
196
|
```
|
193
|
-
|
194
|
-
|
197
|
+
|
198
|
+
### 6. Profile Request
|
195
199
|
The short-lived access token can now be used to request the user's profile. You'll need to make the following request:
|
196
200
|
```
|
197
201
|
curl --request GET \
|
@@ -199,7 +203,7 @@ curl --request GET \
|
|
199
203
|
--header 'authorization: Bearer <access token>' \
|
200
204
|
--header 'content-type: application/json'
|
201
205
|
```
|
202
|
-
|
206
|
+
|
203
207
|
If everything goes well you should receive a JSON response with the user's profile:
|
204
208
|
```
|
205
209
|
{
|
@@ -209,30 +213,30 @@ If everything goes well you should receive a JSON response with the user's profi
|
|
209
213
|
"lastName": "Jackson",
|
210
214
|
}
|
211
215
|
```
|
212
|
-
|
216
|
+
|
213
217
|
- email: The email address of the user as provided by the Identity Provider
|
214
218
|
- id: The id of the user as provided by the Identity Provider
|
215
219
|
- firstName: The first name of the user as provided by the Identity Provider
|
216
220
|
- lastName: The last name of the user as provided by the Identity Provider
|
217
|
-
|
221
|
+
|
218
222
|
## Examples
|
219
223
|
To Do
|
220
|
-
|
224
|
+
|
221
225
|
## Database Support
|
222
226
|
Jackson currently supports the following databases.
|
223
|
-
|
227
|
+
|
224
228
|
- Postgres
|
225
229
|
- CockroachDB
|
226
230
|
- MySQL
|
227
231
|
- MariaDB
|
228
232
|
- MongoDB
|
229
233
|
- Redis
|
230
|
-
|
234
|
+
|
231
235
|
## Configuration
|
232
|
-
Configuration is done via env vars (and in the case of the npm library via an options object).
|
233
|
-
|
236
|
+
Configuration is done via env vars (and in the case of the npm library via an options object).
|
237
|
+
|
234
238
|
The following options are supported and will have to be configured during deployment.
|
235
|
-
|
239
|
+
|
236
240
|
| Key | Description | Default |
|
237
241
|
|-----------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------|
|
238
242
|
| HOST_URL | The URL to bind to | `localhost` |
|
@@ -246,7 +250,7 @@ The following options are supported and will have to be configured during deploy
|
|
246
250
|
| DB_URL (npm: db.url) | The database URL to connect to. For example `postgres://postgres:postgres@localhost:5450/jackson` | |
|
247
251
|
| DB_TYPE (npm: db.type) | Only needed when DB_ENGINE is `sql`. Supported values are `postgres`, `cockroachdb`, `mysql`, `mariadb`. | `postgres` |
|
248
252
|
| 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
|
-
|
253
|
+
|
250
254
|
## Pre-loaded SAML Configuration
|
251
255
|
If PRE_LOADED_CONFIG is set then it should point to a directory with the following structure (example below):-
|
252
256
|
```
|
@@ -265,27 +269,27 @@ module.exports = {
|
|
265
269
|
};
|
266
270
|
```
|
267
271
|
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
|
-
|
272
|
+
|
269
273
|
The config and XML above correspond to the `SAML API config` (see below).
|
270
|
-
|
274
|
+
|
271
275
|
## SAML Login flows
|
272
276
|
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
|
-
|
277
|
+
|
274
278
|
## Contributing
|
275
|
-
Thanks for taking the time to contribute! Contributions are what
|
276
|
-
|
279
|
+
Thanks for taking the time to contribute! Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make will benefit everybody else and are appreciated.
|
280
|
+
|
277
281
|
Please try to create bug reports that are:
|
278
|
-
|
282
|
+
|
279
283
|
- _Reproducible._ Include steps to reproduce the problem.
|
280
284
|
- _Specific._ Include as much detail as possible: which version, what environment, etc.
|
281
285
|
- _Unique._ Do not duplicate existing opened issues.
|
282
286
|
- _Scoped to a Single Bug._ One bug per report.
|
283
|
-
|
287
|
+
|
284
288
|
## Support
|
285
289
|
Reach out to the maintainer at one of the following places:
|
286
|
-
|
290
|
+
|
287
291
|
- [GitHub Issues](https://github.com/boxyhq/jackson/issues)
|
288
292
|
- The email which is located [in GitHub profile](https://github.com/deepakprabhakara)
|
289
|
-
|
293
|
+
|
290
294
|
## License
|
291
295
|
[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.5-beta.
|
3
|
+
"version": "0.1.5-beta.105",
|
4
4
|
"license": "Apache 2.0",
|
5
5
|
"description": "SAML 2.0 service",
|
6
6
|
"main": "src/index.js",
|
@@ -17,7 +17,6 @@
|
|
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
|
-
"calendso": "cross-env DB_URL=postgresql://postgres:postgres@localhost:5450/calendso nodemon src/jackson.js",
|
21
20
|
"mongo": "cross-env DB_ENGINE=mongo DB_URL=mongodb://localhost:27017/jackson nodemon src/jackson.js",
|
22
21
|
"pre-loaded": "cross-env DB_ENGINE=mem PRE_LOADED_CONFIG='./_config' nodemon src/jackson.js",
|
23
22
|
"test": "tap --timeout=100 src/**/*.test.js",
|
package/src/controller/oauth.js
CHANGED
@@ -115,7 +115,7 @@ const authorize = async (req, res) => {
|
|
115
115
|
}
|
116
116
|
|
117
117
|
const samlReq = saml.request({
|
118
|
-
entityID:
|
118
|
+
entityID: options.samlAudience,
|
119
119
|
callbackUrl: options.externalUrl + options.samlPath,
|
120
120
|
signingKey: samlConfig.certs.privateKey,
|
121
121
|
});
|
@@ -196,6 +196,11 @@ const samlResponse = async (req, res) => {
|
|
196
196
|
}
|
197
197
|
|
198
198
|
const profile = await saml.validateAsync(rawResponse, validateOpts);
|
199
|
+
|
200
|
+
// some providers don't return the id in the assertion, we set it to a sha256 hash of the email
|
201
|
+
if (profile && profile.claims && !profile.claims.id) {
|
202
|
+
profile.claims.id = crypto.createHash('sha256').update(profile.claims.email).digest('hex');
|
203
|
+
}
|
199
204
|
|
200
205
|
// store details against a code
|
201
206
|
const code = crypto.randomBytes(20).toString('hex');
|
package/src/index.js
CHANGED
@@ -19,7 +19,7 @@ const defaultOpts = (opts) => {
|
|
19
19
|
newOpts.db = newOpts.db || {};
|
20
20
|
newOpts.db.engine = newOpts.db.engine || 'sql'; // Supported values: redis, sql, mongo, mem. Keep comment in sync with db.js
|
21
21
|
newOpts.db.url =
|
22
|
-
newOpts.db.url || '
|
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
25
|
newOpts.db.limit = (newOpts.db.limit || 1000) * 1; // Limit ttl cleanup to this many items at a time
|
@@ -56,7 +56,7 @@ module.exports = async function (opts) {
|
|
56
56
|
}
|
57
57
|
}
|
58
58
|
|
59
|
-
const type = opts.db.type ? ' Type: ' + opts.db.type : '';
|
59
|
+
const type = opts.db.engine === 'sql' && opts.db.type ? ' Type: ' + opts.db.type : '';
|
60
60
|
console.log(`Using engine: ${opts.db.engine}.${type}`);
|
61
61
|
|
62
62
|
return {
|