@boxyhq/saml-jackson 0.1.5-beta.97 → 0.1.6
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/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +43 -0
- package/.github/pull_request_template.md +31 -0
- package/.github/workflows/main.yml +1 -1
- package/README.md +186 -71
- package/package.json +14 -6
- package/src/controller/api.js +47 -0
- package/src/controller/oauth.js +3 -13
- package/src/controller/utils.js +11 -0
- package/src/db/db.test.js +1 -1
- package/src/db/sql/entity/JacksonStore.js +0 -4
- package/src/db/sql/entity/JacksonTTL.js +23 -0
- package/src/db/sql/model/JacksonStore.js +1 -2
- package/src/db/sql/model/JacksonTTL.js +8 -0
- package/src/db/sql/sql.js +20 -9
- package/src/env.js +3 -0
- package/src/index.js +2 -2
- package/src/jackson.js +28 -1
- package/src/saml/claims.js +40 -0
- package/src/saml/saml.js +29 -3
@@ -0,0 +1,27 @@
|
|
1
|
+
---
|
2
|
+
name: Bug report
|
3
|
+
about: Report any issues with the platform
|
4
|
+
title: ""
|
5
|
+
labels: bug
|
6
|
+
assignees: ""
|
7
|
+
---
|
8
|
+
|
9
|
+
Found a bug? Please fill out the sections below. 👍
|
10
|
+
|
11
|
+
### Issue Summary
|
12
|
+
|
13
|
+
A summary of the issue. This needs to be a clear detailed-rich summary.
|
14
|
+
|
15
|
+
### Steps to Reproduce
|
16
|
+
|
17
|
+
1. (for example) Went to ...
|
18
|
+
2. Clicked on...
|
19
|
+
3. ...
|
20
|
+
|
21
|
+
Any other relevant information. For example, why do you consider this a bug and what did you expect to happen instead?
|
22
|
+
|
23
|
+
### Technical details
|
24
|
+
|
25
|
+
- Browser version: You can use https://www.whatsmybrowser.org/ to find this out.
|
26
|
+
- Node.js version
|
27
|
+
- Anything else that you think could be an issue.
|
@@ -0,0 +1,43 @@
|
|
1
|
+
---
|
2
|
+
name: Feature request
|
3
|
+
about: Suggest a feature or idea
|
4
|
+
title: ""
|
5
|
+
labels: enhancement
|
6
|
+
assignees: ""
|
7
|
+
---
|
8
|
+
|
9
|
+
> Please check if your Feature Request has not been already raised in the [Discussions Tab](https://github.com/boxyhq/jackson/discussions), as we would like to reduce duplicates. If it has been already raised, simply upvote it 🔼.
|
10
|
+
|
11
|
+
### Is your proposal related to a problem?
|
12
|
+
|
13
|
+
<!--
|
14
|
+
Provide a clear and concise description of what the problem is.
|
15
|
+
For example, "I'm always frustrated when..."
|
16
|
+
-->
|
17
|
+
|
18
|
+
(Write your answer here.)
|
19
|
+
|
20
|
+
### Describe the solution you'd like
|
21
|
+
|
22
|
+
<!--
|
23
|
+
Provide a clear and concise description of what you want to happen.
|
24
|
+
-->
|
25
|
+
|
26
|
+
(Describe your proposed solution here.)
|
27
|
+
|
28
|
+
### Describe alternatives you've considered
|
29
|
+
|
30
|
+
<!--
|
31
|
+
Let us know about other solutions you've tried or researched.
|
32
|
+
-->
|
33
|
+
|
34
|
+
(Write your answer here.)
|
35
|
+
|
36
|
+
### Additional context
|
37
|
+
|
38
|
+
<!--
|
39
|
+
Is there anything else you can add about the proposal?
|
40
|
+
You might want to link to related issues here, if you haven't already.
|
41
|
+
-->
|
42
|
+
|
43
|
+
(Write your answer here.)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
## What does this PR do?
|
2
|
+
|
3
|
+
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
4
|
+
|
5
|
+
Fixes # (issue)
|
6
|
+
|
7
|
+
## Type of change
|
8
|
+
|
9
|
+
<!-- Please delete options that are not relevant. -->
|
10
|
+
|
11
|
+
- [ ] Bug fix (non-breaking change which fixes an issue)
|
12
|
+
- [ ] New feature (non-breaking change which adds functionality)
|
13
|
+
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
14
|
+
- [ ] This change requires a documentation update
|
15
|
+
|
16
|
+
## How should this be tested?
|
17
|
+
|
18
|
+
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -->
|
19
|
+
|
20
|
+
- [ ] Test A
|
21
|
+
- [ ] Test B
|
22
|
+
|
23
|
+
## Checklist:
|
24
|
+
|
25
|
+
- [ ] My code follows the style guidelines of this project
|
26
|
+
- [ ] I have performed a self-review of my own code and corrected any misspellings
|
27
|
+
- [ ] I have commented my code, particularly in hard-to-understand areas
|
28
|
+
- [ ] I have made corresponding changes to the documentation
|
29
|
+
- [ ] My changes generate no new warnings
|
30
|
+
- [ ] I have added tests that prove my fix is effective or that my feature works
|
31
|
+
- [ ] New and existing unit tests pass locally with my changes
|
package/README.md
CHANGED
@@ -4,22 +4,34 @@ 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
6
|
|
7
|
-
|
7
|
+
# Source code visualizer
|
8
|
+
|
8
9
|
[CodeSee codebase visualizer](https://app.codesee.io/maps/public/53e91640-23b5-11ec-a724-79d7dd589517)
|
9
10
|
|
10
|
-
#
|
11
|
-
|
11
|
+
# Getting Started
|
12
|
+
|
13
|
+
There are two ways to use this repo.
|
14
|
+
|
15
|
+
- As an npm library (for Express compatible frameworks)
|
16
|
+
- As a separate service
|
17
|
+
|
18
|
+
## Install as an npm library
|
19
|
+
|
20
|
+
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.
|
21
|
+
|
22
|
+
```
|
23
|
+
npm i @boxyhq/saml-jackson
|
24
|
+
```
|
12
25
|
|
13
|
-
|
26
|
+
### Add Express Routes
|
14
27
|
|
15
|
-
Here's how to use the npm library:
|
16
28
|
```
|
17
29
|
// express
|
18
30
|
const express = require('express');
|
19
31
|
const router = express.Router();
|
20
32
|
const cors = require('cors'); // needed if you are calling the token userinfo endpoints from the frontend
|
21
33
|
|
22
|
-
// Set the required options
|
34
|
+
// Set the required options. Refer to https://github.com/boxyhq/jackson#configuration for the full list
|
23
35
|
const opts = {
|
24
36
|
externalUrl: 'https://my-cool-app.com',
|
25
37
|
samlAudience: 'https://my-cool-app.com',
|
@@ -27,15 +39,18 @@ const opts = {
|
|
27
39
|
db: {
|
28
40
|
engine: 'mongo',
|
29
41
|
url: 'mongodb://localhost:27017/my-cool-app',
|
30
|
-
}
|
42
|
+
}
|
31
43
|
};
|
32
44
|
|
45
|
+
|
46
|
+
let apiController;
|
47
|
+
let oauthController;
|
33
48
|
// Please note that the initialization of @boxyhq/saml-jackson is async, you cannot run it at the top level
|
34
49
|
// Run this in a function where you initialise the express server.
|
35
50
|
async function init() {
|
36
51
|
const ret = await require('@boxyhq/saml-jackson')(opts);
|
37
|
-
|
38
|
-
|
52
|
+
apiController = ret.apiController;
|
53
|
+
oauthController = ret.oauthController;
|
39
54
|
}
|
40
55
|
|
41
56
|
// express.js middlewares needed to parse json and x-www-form-urlencoded
|
@@ -82,7 +97,7 @@ router.post('/oauth/token', cors(), async (req, res) => {
|
|
82
97
|
}
|
83
98
|
});
|
84
99
|
|
85
|
-
router.get('/oauth/userinfo',
|
100
|
+
router.get('/oauth/userinfo', async (req, res) => {
|
86
101
|
try {
|
87
102
|
await oauthController.userInfo(req, res);
|
88
103
|
} catch (err) {
|
@@ -91,71 +106,50 @@ router.get('/oauth/userinfo', cors(), async (req, res) => {
|
|
91
106
|
});
|
92
107
|
|
93
108
|
// set the router
|
94
|
-
app.
|
109
|
+
app.use('/sso', router);
|
95
110
|
|
96
111
|
```
|
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
112
|
|
99
|
-
|
100
|
-
|
113
|
+
## Deployment as a service: Docker
|
114
|
+
|
115
|
+
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.
|
101
116
|
|
102
|
-
Example of a docker run:
|
103
117
|
```
|
104
118
|
docker run -p 5000:5000 -p 6000:6000 boxyhq/jackson:78e9099d
|
105
119
|
```
|
106
120
|
|
107
|
-
#
|
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 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. Defaults to `https://saml.boxyhq.com` and is case sensitive. This does not have to 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.
|
121
|
+
Refer to https://github.com/boxyhq/jackson#configuration for the full configuration.
|
142
122
|
|
143
|
-
|
123
|
+
Kubernetes and docker-compose deployment files will be coming soon.
|
144
124
|
|
145
|
-
|
146
|
-
|
125
|
+
## Usage
|
126
|
+
|
127
|
+
### 1. Setting up SAML with your customer's Identity Provider
|
128
|
+
|
129
|
+
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.
|
130
|
+
|
131
|
+
### 1.1 SAML profile/claims/attributes mapping
|
132
|
+
|
133
|
+
As outlined in the guide above we try and support 4 attributes in the SAML claims - `id`, `email`, `firstName`, `lastName`. This is how the common SAML attributes map over for most providers, but some providers have custom mappings. Please refer to the documentation on Identity Provider to understand the exact mapping.
|
134
|
+
|
135
|
+
| SAML Attribute | Jackson mapping |
|
136
|
+
| -------------------------------------------------------------------- | --------------- |
|
137
|
+
| http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier | id |
|
138
|
+
| http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress | email |
|
139
|
+
| http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname | firstName |
|
140
|
+
| http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname | lastName |
|
147
141
|
|
148
|
-
|
149
|
-
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.
|
142
|
+
### 2. SAML config API
|
150
143
|
|
151
|
-
|
152
|
-
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.
|
144
|
+
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.
|
153
145
|
|
154
146
|
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
147
|
|
156
148
|
The following API call sets up the configuration in Jackson:
|
149
|
+
|
157
150
|
```
|
158
151
|
curl --location --request POST 'http://localhost:6000/api/v1/saml/config' \
|
152
|
+
--header 'Authorization: Api-Key <Jackson API Key>' \
|
159
153
|
--header 'Content-Type: application/x-www-form-urlencoded' \
|
160
154
|
--data-urlencode 'rawMetadata=<IdP/SP metadata XML>' \
|
161
155
|
--data-urlencode 'defaultRedirectUrl=http://localhost:3000/login/saml' \
|
@@ -170,16 +164,41 @@ curl --location --request POST 'http://localhost:6000/api/v1/saml/config' \
|
|
170
164
|
- 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
165
|
- 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
166
|
|
173
|
-
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=
|
167
|
+
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=tenant=<tenantID>&product=<productID>` and any arbitrary value for `client_secret` when setting up the OAuth 2.0 flow. Additionally a `provider` attribute is also returned which indicates the domain of your Identity Provider.
|
168
|
+
|
169
|
+
#### 2.1 SAML get config API
|
170
|
+
|
171
|
+
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.
|
172
|
+
|
173
|
+
```
|
174
|
+
curl --location --request POST 'http://localhost:6000/api/v1/saml/config/get' \
|
175
|
+
--header 'Authorization: Api-Key <Jackson API Key>' \
|
176
|
+
--header 'Content-Type: application/x-www-form-urlencoded' \
|
177
|
+
--data-urlencode 'tenant=boxyhq.com' \
|
178
|
+
--data-urlencode 'product=demo'
|
179
|
+
```
|
180
|
+
|
181
|
+
```
|
182
|
+
curl --location --request POST 'http://localhost:6000/api/v1/saml/config/get' \
|
183
|
+
--header 'Authorization: Api-Key <Jackson API Key>' \
|
184
|
+
--header 'Content-Type: application/x-www-form-urlencoded' \
|
185
|
+
--data-urlencode 'clientID=<Client ID>'
|
186
|
+
```
|
187
|
+
|
188
|
+
The response returns a JSON with `provider` indicating the domain of your Identity Provider. If an empty JSON payload is returned then we do not have any configuration stored for the attributes you requested.
|
189
|
+
|
190
|
+
### 3. OAuth 2.0 Flow
|
174
191
|
|
175
|
-
# OAuth 2.0 Flow
|
176
192
|
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
193
|
|
178
194
|
Jackson also supports the PKCE authorization flow (https://oauth.net/2/pkce/), so you can protect your SPAs.
|
179
195
|
|
180
196
|
If for any reason you need to implement the flow on your own, the steps are outlined below:
|
181
|
-
|
197
|
+
|
198
|
+
### 4. Authorize
|
199
|
+
|
182
200
|
The OAuth flow begins with redirecting your user to the `authorize` URL:
|
201
|
+
|
183
202
|
```
|
184
203
|
https://localhost:5000/oauth/authorize
|
185
204
|
?response_type=code&provider=saml
|
@@ -189,30 +208,34 @@ https://localhost:5000/oauth/authorize
|
|
189
208
|
```
|
190
209
|
|
191
210
|
- response_type=code: This is the only supported type for now but maybe extended in the future
|
192
|
-
- client_id: Use the client_id returned by the SAML config API or use `
|
211
|
+
- 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`.
|
193
212
|
- redirect_uri: This is where the user will be taken back once the authorization flow is complete
|
194
213
|
- 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
214
|
|
196
|
-
|
197
|
-
|
215
|
+
### 5. Code Exchange
|
216
|
+
|
217
|
+
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
218
|
|
199
219
|
The code can then be exchanged for a token by making the following request:
|
220
|
+
|
200
221
|
```
|
201
222
|
curl --request POST \
|
202
223
|
--url 'http://localhost:5000/oauth/token' \
|
203
224
|
--header 'content-type: application/x-www-form-urlencoded' \
|
204
|
-
--data grant_type=authorization_code \
|
225
|
+
--data 'grant_type=authorization_code' \
|
205
226
|
--data 'client_id=<clientID or tenant and product query params as described in the SAML config API section above>' \
|
206
|
-
--data client_secret=<clientSecret or any arbitrary value if using the tenant and product in the clientID> \
|
227
|
+
--data 'client_secret=<clientSecret or any arbitrary value if using the tenant and product in the clientID>' \
|
207
228
|
--data 'redirect_uri=<redirect URL>' \
|
208
|
-
--data code=<code from the query parameter above>
|
229
|
+
--data 'code=<code from the query parameter above>'
|
209
230
|
```
|
231
|
+
|
210
232
|
- grant_type=authorization_code: This is the only supported flow, for now. We might extend this in the future
|
211
|
-
- client_id: Use the client_id returned by the SAML config API or use `
|
233
|
+
- 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`.
|
212
234
|
- 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
|
213
235
|
- 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
|
214
236
|
|
215
237
|
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.
|
238
|
+
|
216
239
|
```
|
217
240
|
{
|
218
241
|
"access_token": <access token>,
|
@@ -221,8 +244,10 @@ If everything goes well you should receive a JSON response that includes the acc
|
|
221
244
|
}
|
222
245
|
```
|
223
246
|
|
224
|
-
|
247
|
+
### 6. Profile Request
|
248
|
+
|
225
249
|
The short-lived access token can now be used to request the user's profile. You'll need to make the following request:
|
250
|
+
|
226
251
|
```
|
227
252
|
curl --request GET \
|
228
253
|
--url https://localhost:5000/oauth/me \
|
@@ -231,16 +256,106 @@ curl --request GET \
|
|
231
256
|
```
|
232
257
|
|
233
258
|
If everything goes well you should receive a JSON response with the user's profile:
|
259
|
+
|
234
260
|
```
|
235
261
|
{
|
262
|
+
"id": <id from the Identity Provider>,
|
236
263
|
"email": "sjackson@coolstartup.com",
|
237
264
|
"firstName": "SAML"
|
238
|
-
"
|
239
|
-
"lastName": "Jackson",
|
265
|
+
"lastName": "Jackson"
|
240
266
|
}
|
241
267
|
```
|
242
268
|
|
243
|
-
- email: The email address of the user as provided by the Identity Provider
|
244
269
|
- id: The id of the user as provided by the Identity Provider
|
270
|
+
- email: The email address of the user as provided by the Identity Provider
|
245
271
|
- firstName: The first name of the user as provided by the Identity Provider
|
246
272
|
- lastName: The last name of the user as provided by the Identity Provider
|
273
|
+
|
274
|
+
## Examples
|
275
|
+
|
276
|
+
To Do
|
277
|
+
|
278
|
+
## Database Support
|
279
|
+
|
280
|
+
Jackson currently supports the following databases.
|
281
|
+
|
282
|
+
- Postgres
|
283
|
+
- CockroachDB
|
284
|
+
- MySQL
|
285
|
+
- MariaDB
|
286
|
+
- MongoDB
|
287
|
+
- Redis
|
288
|
+
|
289
|
+
## Configuration
|
290
|
+
|
291
|
+
Configuration is done via env vars (and in the case of the npm library via an options object).
|
292
|
+
|
293
|
+
The following options are supported and will have to be configured during deployment.
|
294
|
+
|
295
|
+
| Key | Description | Default |
|
296
|
+
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
|
297
|
+
| HOST_URL | The URL to bind to | `localhost` |
|
298
|
+
| HOST_PORT | The port to bind to | `5000` |
|
299
|
+
| EXTERNAL_URL (npm: externalUrl) | The public URL to reach this service, used internally for documenting the SAML configuration instructions. | `http://{HOST_URL}:{HOST_PORT}` |
|
300
|
+
| INTERNAL_HOST_URL | The URL to bind to expose the internal APIs. Do not configure this to a public network. | `localhost` |
|
301
|
+
| INTERNAL_HOST_PORT | The port to bind to for the internal APIs. | `6000` |
|
302
|
+
| JACKSON_API_KEYS | A comma separated list of API keys that will be validated when serving the Config API requests | |
|
303
|
+
| 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` |
|
304
|
+
| 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` |
|
305
|
+
| DB_ENGINE (npm: db.engine) | Supported values are `redis`, `sql`, `mongo`, `mem`. | `sql` |
|
306
|
+
| DB_URL (npm: db.url) | The database URL to connect to. For example `postgres://postgres:postgres@localhost:5450/jackson` | |
|
307
|
+
| DB_TYPE (npm: db.type) | Only needed when DB_ENGINE is `sql`. Supported values are `postgres`, `cockroachdb`, `mysql`, `mariadb`. | `postgres` |
|
308
|
+
| 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. | |
|
309
|
+
|
310
|
+
## Pre-loaded SAML Configuration
|
311
|
+
|
312
|
+
If PRE_LOADED_CONFIG is set then it should point to a directory with the following structure (example below):-
|
313
|
+
|
314
|
+
```
|
315
|
+
boxyhq.js
|
316
|
+
boxyhq.xml
|
317
|
+
anothertenant.js
|
318
|
+
anothertenant.xml
|
319
|
+
```
|
320
|
+
|
321
|
+
The JS file has the following structure:-
|
322
|
+
|
323
|
+
```
|
324
|
+
module.exports = {
|
325
|
+
defaultRedirectUrl: 'http://localhost:3000/login/saml',
|
326
|
+
redirectUrl: '["http://localhost:3000/*", "http://localhost:5000/*"]',
|
327
|
+
tenant: 'boxyhq.com',
|
328
|
+
product: 'demo',
|
329
|
+
};
|
330
|
+
```
|
331
|
+
|
332
|
+
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.
|
333
|
+
|
334
|
+
The config and XML above correspond to the `SAML API config` (see below).
|
335
|
+
|
336
|
+
## SAML Login flows
|
337
|
+
|
338
|
+
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/.
|
339
|
+
|
340
|
+
## Contributing
|
341
|
+
|
342
|
+
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.
|
343
|
+
|
344
|
+
Please try to create bug reports that are:
|
345
|
+
|
346
|
+
- _Reproducible._ Include steps to reproduce the problem.
|
347
|
+
- _Specific._ Include as much detail as possible: which version, what environment, etc.
|
348
|
+
- _Unique._ Do not duplicate existing opened issues.
|
349
|
+
- _Scoped to a Single Bug._ One bug per report.
|
350
|
+
|
351
|
+
## Support
|
352
|
+
|
353
|
+
Reach out to the maintainer at one of the following places:
|
354
|
+
|
355
|
+
- [GitHub Discussions](https://github.com/boxyhq/jackson/discussions)
|
356
|
+
- [GitHub Issues](https://github.com/boxyhq/jackson/issues)
|
357
|
+
- The email which is located [in GitHub profile](https://github.com/deepakprabhakara)
|
358
|
+
|
359
|
+
## License
|
360
|
+
|
361
|
+
[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
|
+
"version": "0.1.6",
|
4
4
|
"license": "Apache 2.0",
|
5
5
|
"description": "SAML 2.0 service",
|
6
6
|
"main": "src/index.js",
|
@@ -17,11 +17,12 @@
|
|
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",
|
22
|
+
"pre-loaded-db": "cross-env PRE_LOADED_CONFIG='./_config' nodemon src/jackson.js",
|
23
23
|
"test": "tap --timeout=100 src/**/*.test.js",
|
24
|
-
"dev-dbs": "docker-compose -f ./_dev/docker-compose.yml up -d"
|
24
|
+
"dev-dbs": "docker-compose -f ./_dev/docker-compose.yml up -d",
|
25
|
+
"dev-dbs-destroy": "docker-compose -f ./_dev/docker-compose.yml down --volumes --remove-orphans"
|
25
26
|
},
|
26
27
|
"tap": {
|
27
28
|
"coverage-map": "map.js",
|
@@ -36,7 +37,7 @@
|
|
36
37
|
"@peculiar/x509": "1.6.0",
|
37
38
|
"cors": "2.8.5",
|
38
39
|
"express": "4.17.1",
|
39
|
-
"mongodb": "4.2.
|
40
|
+
"mongodb": "4.2.1",
|
40
41
|
"mysql2": "2.3.3",
|
41
42
|
"pg": "8.7.1",
|
42
43
|
"rambda": "6.9.0",
|
@@ -51,8 +52,15 @@
|
|
51
52
|
},
|
52
53
|
"devDependencies": {
|
53
54
|
"cross-env": "7.0.3",
|
54
|
-
"eslint": "8.
|
55
|
+
"eslint": "8.4.0",
|
56
|
+
"husky": "7.0.4",
|
57
|
+
"lint-staged": "12.1.2",
|
55
58
|
"nodemon": "2.0.15",
|
56
|
-
"
|
59
|
+
"prettier": "2.5.1",
|
60
|
+
"tap": "15.1.5"
|
61
|
+
},
|
62
|
+
"lint-staged": {
|
63
|
+
"*.js": "eslint --cache --fix",
|
64
|
+
"*.{js,css,md}": "prettier --write"
|
57
65
|
}
|
58
66
|
}
|
package/src/controller/api.js
CHANGED
@@ -7,11 +7,33 @@ const crypto = require('crypto');
|
|
7
7
|
|
8
8
|
let configStore;
|
9
9
|
|
10
|
+
const extractHostName = (url) => {
|
11
|
+
try {
|
12
|
+
const pUrl = new URL(url);
|
13
|
+
if(pUrl.hostname.startsWith('www.')) {
|
14
|
+
return pUrl.hostname.substring(4);
|
15
|
+
}
|
16
|
+
return pUrl.hostname;
|
17
|
+
} catch (err) {
|
18
|
+
return null;
|
19
|
+
}
|
20
|
+
};
|
21
|
+
|
10
22
|
const config = async (body) => {
|
11
23
|
const { rawMetadata, defaultRedirectUrl, redirectUrl, tenant, product } =
|
12
24
|
body;
|
13
25
|
const idpMetadata = await saml.parseMetadataAsync(rawMetadata);
|
14
26
|
|
27
|
+
// extract provider
|
28
|
+
let providerName = extractHostName(idpMetadata.entityID);
|
29
|
+
if (!providerName) {
|
30
|
+
providerName = extractHostName(
|
31
|
+
idpMetadata.sso.redirectUrl || idpMetadata.sso.postUrl
|
32
|
+
);
|
33
|
+
}
|
34
|
+
|
35
|
+
idpMetadata.provider = providerName ? providerName : 'Unknown';
|
36
|
+
|
15
37
|
let clientID = dbutils.keyDigest(
|
16
38
|
dbutils.keyFromParts(tenant, product, idpMetadata.entityID)
|
17
39
|
);
|
@@ -56,12 +78,37 @@ const config = async (body) => {
|
|
56
78
|
return {
|
57
79
|
client_id: clientID,
|
58
80
|
client_secret: clientSecret,
|
81
|
+
provider: idpMetadata.provider,
|
59
82
|
};
|
60
83
|
};
|
61
84
|
|
85
|
+
const getConfig = async (body) => {
|
86
|
+
const { clientID, tenant, product } = body;
|
87
|
+
|
88
|
+
if (clientID) {
|
89
|
+
const samlConfig = await configStore.get(clientID);
|
90
|
+
if (!samlConfig) {
|
91
|
+
return {};
|
92
|
+
}
|
93
|
+
|
94
|
+
return { provider: samlConfig.idpMetadata.provider };
|
95
|
+
} else {
|
96
|
+
const samlConfigs = await configStore.getByIndex({
|
97
|
+
name: indexNames.tenantProduct,
|
98
|
+
value: dbutils.keyFromParts(tenant, product),
|
99
|
+
});
|
100
|
+
if (!samlConfigs || !samlConfigs.length) {
|
101
|
+
return {};
|
102
|
+
}
|
103
|
+
|
104
|
+
return { provider: samlConfigs[0].idpMetadata.provider };
|
105
|
+
}
|
106
|
+
};
|
107
|
+
|
62
108
|
module.exports = (opts) => {
|
63
109
|
configStore = opts.configStore;
|
64
110
|
return {
|
65
111
|
config,
|
112
|
+
getConfig,
|
66
113
|
};
|
67
114
|
};
|
package/src/controller/oauth.js
CHANGED
@@ -2,7 +2,7 @@ const crypto = require('crypto');
|
|
2
2
|
|
3
3
|
const saml = require('../saml/saml.js');
|
4
4
|
const codeVerifier = require('./oauth/code-verifier.js');
|
5
|
-
const { indexNames } = require('./utils.js');
|
5
|
+
const { indexNames, extractAuthToken } = require('./utils.js');
|
6
6
|
const dbutils = require('../db/utils.js');
|
7
7
|
const redirect = require('./oauth/redirect.js');
|
8
8
|
const allowed = require('./oauth/allowed.js');
|
@@ -15,16 +15,6 @@ let options;
|
|
15
15
|
|
16
16
|
const relayStatePrefix = 'boxyhq_jackson_';
|
17
17
|
|
18
|
-
const extractBearerToken = (req) => {
|
19
|
-
const authHeader = req.get('authorization');
|
20
|
-
const parts = (authHeader || '').split(' ');
|
21
|
-
if (parts.length > 1) {
|
22
|
-
return parts[1];
|
23
|
-
}
|
24
|
-
|
25
|
-
return null;
|
26
|
-
};
|
27
|
-
|
28
18
|
function getEncodedClientId(client_id) {
|
29
19
|
try {
|
30
20
|
const sp = new URLSearchParams(client_id);
|
@@ -115,7 +105,7 @@ const authorize = async (req, res) => {
|
|
115
105
|
}
|
116
106
|
|
117
107
|
const samlReq = saml.request({
|
118
|
-
entityID:
|
108
|
+
entityID: options.samlAudience,
|
119
109
|
callbackUrl: options.externalUrl + options.samlPath,
|
120
110
|
signingKey: samlConfig.certs.privateKey,
|
121
111
|
});
|
@@ -298,7 +288,7 @@ const token = async (req, res) => {
|
|
298
288
|
};
|
299
289
|
|
300
290
|
const userInfo = async (req, res) => {
|
301
|
-
let token =
|
291
|
+
let token = extractAuthToken(req);
|
302
292
|
|
303
293
|
// check for query param
|
304
294
|
if (!token) {
|
package/src/controller/utils.js
CHANGED
@@ -3,6 +3,17 @@ const indexNames = {
|
|
3
3
|
tenantProduct: 'tenantProduct',
|
4
4
|
};
|
5
5
|
|
6
|
+
const extractAuthToken = (req) => {
|
7
|
+
const authHeader = req.get('authorization');
|
8
|
+
const parts = (authHeader || '').split(' ');
|
9
|
+
if (parts.length > 1) {
|
10
|
+
return parts[1];
|
11
|
+
}
|
12
|
+
|
13
|
+
return null;
|
14
|
+
};
|
15
|
+
|
6
16
|
module.exports = {
|
7
17
|
indexNames,
|
18
|
+
extractAuthToken,
|
8
19
|
};
|
package/src/db/db.test.js
CHANGED
@@ -224,7 +224,7 @@ t.test('dbs', ({ end }) => {
|
|
224
224
|
}
|
225
225
|
|
226
226
|
await new Promise((resolve) =>
|
227
|
-
setTimeout(resolve, (
|
227
|
+
setTimeout(resolve, (2*ttl + 0.5) * 1000)
|
228
228
|
);
|
229
229
|
|
230
230
|
const ret1 = await ttlStore.get(record1.id);
|
@@ -0,0 +1,23 @@
|
|
1
|
+
const EntitySchema = require('typeorm').EntitySchema;
|
2
|
+
const JacksonTTL = require('../model/JacksonTTL.js');
|
3
|
+
|
4
|
+
module.exports = new EntitySchema({
|
5
|
+
name: 'JacksonTTL',
|
6
|
+
target: JacksonTTL,
|
7
|
+
columns: {
|
8
|
+
key: {
|
9
|
+
primary: true,
|
10
|
+
type: 'varchar',
|
11
|
+
length: 1500,
|
12
|
+
},
|
13
|
+
expiresAt: {
|
14
|
+
type: 'bigint',
|
15
|
+
},
|
16
|
+
},
|
17
|
+
indices: [
|
18
|
+
{
|
19
|
+
name: '_jackson_ttl_expires_at',
|
20
|
+
columns: ['expiresAt'],
|
21
|
+
},
|
22
|
+
],
|
23
|
+
});
|
package/src/db/sql/sql.js
CHANGED
@@ -2,6 +2,7 @@ require('reflect-metadata');
|
|
2
2
|
const typeorm = require('typeorm');
|
3
3
|
const JacksonStore = require('./model/JacksonStore.js');
|
4
4
|
const JacksonIndex = require('./model/JacksonIndex.js');
|
5
|
+
const JacksonTTL = require('./model/JacksonTTL.js');
|
5
6
|
|
6
7
|
const dbutils = require('../utils.js');
|
7
8
|
|
@@ -20,6 +21,7 @@ class Sql {
|
|
20
21
|
entities: [
|
21
22
|
require('./entity/JacksonStore.js')(options.type),
|
22
23
|
require('./entity/JacksonIndex.js'),
|
24
|
+
require('./entity/JacksonTTL.js'),
|
23
25
|
],
|
24
26
|
});
|
25
27
|
|
@@ -33,22 +35,29 @@ class Sql {
|
|
33
35
|
|
34
36
|
this.storeRepository = this.connection.getRepository(JacksonStore);
|
35
37
|
this.indexRepository = this.connection.getRepository(JacksonIndex);
|
38
|
+
this.ttlRepository = this.connection.getRepository(JacksonTTL);
|
36
39
|
|
37
40
|
if (options.ttl && options.limit) {
|
38
41
|
this.ttlCleanup = async () => {
|
39
42
|
const now = Date.now();
|
40
43
|
|
41
44
|
while (true) {
|
42
|
-
const ids = await this.
|
43
|
-
|
44
|
-
|
45
|
-
|
45
|
+
const ids = await this.ttlRepository
|
46
|
+
.createQueryBuilder('jackson_ttl')
|
47
|
+
.limit(options.limit)
|
48
|
+
.where('jackson_ttl.expiresAt <= :expiresAt', { expiresAt: now })
|
49
|
+
.getMany();
|
46
50
|
|
47
51
|
if (ids.length <= 0) {
|
48
52
|
break;
|
49
53
|
}
|
50
54
|
|
55
|
+
const delIds = ids.map((id) => {
|
56
|
+
return id.key;
|
57
|
+
});
|
58
|
+
|
51
59
|
await this.storeRepository.remove(ids);
|
60
|
+
await this.ttlRepository.delete(delIds);
|
52
61
|
}
|
53
62
|
|
54
63
|
this.timerId = setTimeout(this.ttlCleanup, options.ttl * 1000);
|
@@ -99,13 +108,15 @@ class Sql {
|
|
99
108
|
|
100
109
|
async put(namespace, key, val, ttl = 0, ...indexes) {
|
101
110
|
await this.connection.transaction(async (transactionalEntityManager) => {
|
102
|
-
const
|
103
|
-
|
104
|
-
JSON.stringify(val),
|
105
|
-
ttl > 0 ? Date.now() + ttl * 1000 : null
|
106
|
-
);
|
111
|
+
const dbKey = dbutils.key(namespace, key);
|
112
|
+
const store = new JacksonStore(dbKey, JSON.stringify(val));
|
107
113
|
await transactionalEntityManager.save(store);
|
108
114
|
|
115
|
+
if (ttl) {
|
116
|
+
const ttlRec = new JacksonTTL(dbKey, Date.now() + ttl * 1000);
|
117
|
+
await transactionalEntityManager.save(ttlRec);
|
118
|
+
}
|
119
|
+
|
109
120
|
// no ttl support for secondary indexes
|
110
121
|
for (const idx of indexes || []) {
|
111
122
|
const key = dbutils.keyForIndex(namespace, idx);
|
package/src/env.js
CHANGED
@@ -7,6 +7,8 @@ const samlPath = process.env.SAML_PATH || '/oauth/saml';
|
|
7
7
|
const internalHostUrl = process.env.INTERNAL_HOST_URL || 'localhost';
|
8
8
|
const internalHostPort = (process.env.INTERNAL_HOST_PORT || '6000') * 1;
|
9
9
|
|
10
|
+
const apiKeys = (process.env.JACKSON_API_KEYS || '').split(',');
|
11
|
+
|
10
12
|
const samlAudience = process.env.SAML_AUDIENCE;
|
11
13
|
const preLoadedConfig = process.env.PRE_LOADED_CONFIG;
|
12
14
|
|
@@ -27,6 +29,7 @@ module.exports = {
|
|
27
29
|
preLoadedConfig,
|
28
30
|
internalHostUrl,
|
29
31
|
internalHostPort,
|
32
|
+
apiKeys,
|
30
33
|
idpEnabled,
|
31
34
|
db,
|
32
35
|
useInternalServer: !(
|
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 {
|
package/src/jackson.js
CHANGED
@@ -2,6 +2,7 @@ const express = require('express');
|
|
2
2
|
const cors = require('cors');
|
3
3
|
|
4
4
|
const env = require('./env.js');
|
5
|
+
const { extractAuthToken } = require('./controller/utils.js');
|
5
6
|
|
6
7
|
let apiController;
|
7
8
|
let oauthController;
|
@@ -38,7 +39,7 @@ app.post(oauthPath + '/token', cors(), async (req, res) => {
|
|
38
39
|
}
|
39
40
|
});
|
40
41
|
|
41
|
-
app.get(oauthPath + '/userinfo',
|
42
|
+
app.get(oauthPath + '/userinfo', async (req, res) => {
|
42
43
|
try {
|
43
44
|
await oauthController.userInfo(req, res);
|
44
45
|
} catch (err) {
|
@@ -66,8 +67,18 @@ if (env.useInternalServer) {
|
|
66
67
|
internalApp.use(express.urlencoded({ extended: true }));
|
67
68
|
}
|
68
69
|
|
70
|
+
const validateApiKey = (token) => {
|
71
|
+
return env.apiKeys.includes(token);
|
72
|
+
};
|
73
|
+
|
69
74
|
internalApp.post(apiPath + '/config', async (req, res) => {
|
70
75
|
try {
|
76
|
+
const apiKey = extractAuthToken(req);
|
77
|
+
if (!validateApiKey(apiKey)) {
|
78
|
+
res.status(401).send('Unauthorized');
|
79
|
+
return;
|
80
|
+
}
|
81
|
+
|
71
82
|
res.json(await apiController.config(req.body));
|
72
83
|
} catch (err) {
|
73
84
|
res.status(500).json({
|
@@ -76,6 +87,22 @@ internalApp.post(apiPath + '/config', async (req, res) => {
|
|
76
87
|
}
|
77
88
|
});
|
78
89
|
|
90
|
+
internalApp.post(apiPath + '/config/get', async (req, res) => {
|
91
|
+
try {
|
92
|
+
const apiKey = extractAuthToken(req);
|
93
|
+
if (!validateApiKey(apiKey)) {
|
94
|
+
res.status(401).send('Unauthorized');
|
95
|
+
return;
|
96
|
+
}
|
97
|
+
|
98
|
+
res.json(await apiController.getConfig(req.body));
|
99
|
+
} catch (err) {
|
100
|
+
res.status(500).json({
|
101
|
+
error: err.message,
|
102
|
+
});
|
103
|
+
}
|
104
|
+
});
|
105
|
+
|
79
106
|
let internalServer = server;
|
80
107
|
if (env.useInternalServer) {
|
81
108
|
internalServer = internalApp.listen(env.internalHostPort, async () => {
|
@@ -0,0 +1,40 @@
|
|
1
|
+
const mapping = [
|
2
|
+
{
|
3
|
+
attribute: 'id',
|
4
|
+
schema:
|
5
|
+
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier',
|
6
|
+
},
|
7
|
+
{
|
8
|
+
attribute: 'email',
|
9
|
+
schema:
|
10
|
+
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
|
11
|
+
},
|
12
|
+
{
|
13
|
+
attribute: 'firstName',
|
14
|
+
schema: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname',
|
15
|
+
},
|
16
|
+
{
|
17
|
+
attribute: 'lastName',
|
18
|
+
schema: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname',
|
19
|
+
},
|
20
|
+
];
|
21
|
+
|
22
|
+
const map = (claims) => {
|
23
|
+
const profile = {
|
24
|
+
raw: claims,
|
25
|
+
};
|
26
|
+
|
27
|
+
mapping.forEach((m) => {
|
28
|
+
if (claims[m.attribute]) {
|
29
|
+
profile[m.attribute] = claims[m.attribute];
|
30
|
+
} else if (claims[m.schema]) {
|
31
|
+
profile[m.attribute] = claims[m.schema];
|
32
|
+
}
|
33
|
+
});
|
34
|
+
|
35
|
+
return profile;
|
36
|
+
};
|
37
|
+
|
38
|
+
module.exports = {
|
39
|
+
map,
|
40
|
+
};
|
package/src/saml/saml.js
CHANGED
@@ -5,6 +5,7 @@ const thumbprint = require('thumbprint');
|
|
5
5
|
const xmlbuilder = require('xmlbuilder');
|
6
6
|
const crypto = require('crypto');
|
7
7
|
const xmlcrypto = require('xml-crypto');
|
8
|
+
const claims = require('./claims');
|
8
9
|
|
9
10
|
const idPrefix = '_';
|
10
11
|
const authnXPath =
|
@@ -120,6 +121,19 @@ module.exports = {
|
|
120
121
|
return;
|
121
122
|
}
|
122
123
|
|
124
|
+
if (profile && profile.claims) {
|
125
|
+
// we map claims to our attributes id, email, firstName, lastName where possible. We also map original claims to raw
|
126
|
+
profile.claims = claims.map(profile.claims);
|
127
|
+
|
128
|
+
// some providers don't return the id in the assertion, we set it to a sha256 hash of the email
|
129
|
+
if (!profile.claims.id) {
|
130
|
+
profile.claims.id = crypto
|
131
|
+
.createHash('sha256')
|
132
|
+
.update(profile.claims.email)
|
133
|
+
.digest('hex');
|
134
|
+
}
|
135
|
+
}
|
136
|
+
|
123
137
|
resolve(profile);
|
124
138
|
}
|
125
139
|
);
|
@@ -141,12 +155,20 @@ module.exports = {
|
|
141
155
|
let X509Certificate = null;
|
142
156
|
let ssoPostUrl = null;
|
143
157
|
let ssoRedirectUrl = null;
|
158
|
+
let loginType = 'idp';
|
144
159
|
|
145
|
-
|
146
|
-
|
160
|
+
let ssoDes = rambda.pathOr(
|
161
|
+
null,
|
147
162
|
'EntityDescriptor.IDPSSODescriptor',
|
148
163
|
res
|
149
164
|
);
|
165
|
+
if (!ssoDes) {
|
166
|
+
ssoDes = rambda.pathOr([], 'EntityDescriptor.SPSSODescriptor', res);
|
167
|
+
if (!ssoDes) {
|
168
|
+
loginType = 'sp';
|
169
|
+
}
|
170
|
+
}
|
171
|
+
|
150
172
|
for (const ssoDesRec of ssoDes) {
|
151
173
|
const keyDes = ssoDesRec['KeyDescriptor'];
|
152
174
|
for (const keyDesRec of keyDes) {
|
@@ -157,7 +179,10 @@ module.exports = {
|
|
157
179
|
}
|
158
180
|
}
|
159
181
|
|
160
|
-
const ssoSvc =
|
182
|
+
const ssoSvc =
|
183
|
+
ssoDesRec['SingleSignOnService'] ||
|
184
|
+
ssoDesRec['AssertionConsumerService'] ||
|
185
|
+
[];
|
161
186
|
for (const ssoSvcRec of ssoSvc) {
|
162
187
|
if (
|
163
188
|
rambda.pathOr('', '$.Binding', ssoSvcRec).endsWith('HTTP-POST')
|
@@ -188,6 +213,7 @@ module.exports = {
|
|
188
213
|
if (ssoRedirectUrl) {
|
189
214
|
ret.sso.redirectUrl = ssoRedirectUrl;
|
190
215
|
}
|
216
|
+
ret.loginType = loginType;
|
191
217
|
|
192
218
|
resolve(ret);
|
193
219
|
}
|