@boxyhq/saml-jackson 0.1.5-beta.124 → 0.1.5-beta.137
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -8
- package/package.json +3 -2
- package/src/controller/api.js +46 -0
- package/src/jackson.js +16 -0
package/README.md
CHANGED
@@ -129,14 +129,15 @@ Kubernetes and docker-compose deployment files will be coming soon.
|
|
129
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
130
|
|
131
131
|
### 1.1 SAML profile/claims/attributes mapping
|
132
|
-
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 aattributes map over for most providers, but some providers have custom mappings. Please refer to the documentation on Identity Provider to understand the exact mapping.
|
133
132
|
|
134
|
-
|
135
|
-
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|http://schemas.xmlsoap.org/ws/2005/05/identity/claims/
|
139
|
-
|http://schemas.xmlsoap.org/ws/2005/05/identity/claims/
|
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 |
|
140
141
|
|
141
142
|
### 2. SAML config API
|
142
143
|
|
@@ -148,6 +149,7 @@ The following API call sets up the configuration in Jackson:
|
|
148
149
|
|
149
150
|
```
|
150
151
|
curl --location --request POST 'http://localhost:6000/api/v1/saml/config' \
|
152
|
+
--header 'Authorization: Api-Key <Jackson API Key>' \
|
151
153
|
--header 'Content-Type: application/x-www-form-urlencoded' \
|
152
154
|
--data-urlencode 'rawMetadata=<IdP/SP metadata XML>' \
|
153
155
|
--data-urlencode 'defaultRedirectUrl=http://localhost:3000/login/saml' \
|
@@ -162,7 +164,29 @@ curl --location --request POST 'http://localhost:6000/api/v1/saml/config' \
|
|
162
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
|
163
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
|
164
166
|
|
165
|
-
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.
|
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` and `clientSecret` combination or `tenant` and `product` combination.
|
172
|
+
|
173
|
+
```
|
174
|
+
curl --location --request GET 'http://localhost:6000/api/v1/saml/config' \
|
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 GET 'http://localhost:6000/api/v1/saml/config' \
|
183
|
+
--header 'Authorization: Api-Key <Jackson API Key>' \
|
184
|
+
--header 'Content-Type: application/x-www-form-urlencoded' \
|
185
|
+
--data-urlencode 'clientID=<Client ID>' \
|
186
|
+
--data-urlencode 'clientSecret=<Client Secret>'
|
187
|
+
```
|
188
|
+
|
189
|
+
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.
|
166
190
|
|
167
191
|
### 3. OAuth 2.0 Flow
|
168
192
|
|
@@ -276,6 +300,7 @@ The following options are supported and will have to be configured during deploy
|
|
276
300
|
| EXTERNAL_URL (npm: externalUrl) | The public URL to reach this service, used internally for documenting the SAML configuration instructions. | `http://{HOST_URL}:{HOST_PORT}` |
|
277
301
|
| INTERNAL_HOST_URL | The URL to bind to expose the internal APIs. Do not configure this to a public network. | `localhost` |
|
278
302
|
| INTERNAL_HOST_PORT | The port to bind to for the internal APIs. | `6000` |
|
303
|
+
| JACKSON_API_KEYS | A comma separated list of API keys that will be validated when serving the Config API requests | |
|
279
304
|
| 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` |
|
280
305
|
| 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` |
|
281
306
|
| DB_ENGINE (npm: db.engine) | Supported values are `redis`, `sql`, `mongo`, `mem`. | `sql` |
|
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.137",
|
4
4
|
"license": "Apache 2.0",
|
5
5
|
"description": "SAML 2.0 service",
|
6
6
|
"main": "src/index.js",
|
@@ -20,7 +20,8 @@
|
|
20
20
|
"mongo": "cross-env DB_ENGINE=mongo DB_URL=mongodb://localhost:27017/jackson nodemon src/jackson.js",
|
21
21
|
"pre-loaded": "cross-env DB_ENGINE=mem PRE_LOADED_CONFIG='./_config' nodemon src/jackson.js",
|
22
22
|
"test": "tap --timeout=100 src/**/*.test.js",
|
23
|
-
"dev-dbs": "docker-compose -f ./_dev/docker-compose.yml up -d"
|
23
|
+
"dev-dbs": "docker-compose -f ./_dev/docker-compose.yml up -d",
|
24
|
+
"dev-dbs-destroy": "docker-compose -f ./_dev/docker-compose.yml down --volumes --remove-orphans"
|
24
25
|
},
|
25
26
|
"tap": {
|
26
27
|
"coverage-map": "map.js",
|
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
|
);
|
@@ -59,9 +81,33 @@ const config = async (body) => {
|
|
59
81
|
};
|
60
82
|
};
|
61
83
|
|
84
|
+
const getConfig = async (body) => {
|
85
|
+
const { clientID, clientSecret, tenant, product } = body;
|
86
|
+
|
87
|
+
if (clientID && clientSecret) {
|
88
|
+
const samlConfig = await configStore.get(clientID);
|
89
|
+
if (!samlConfig) {
|
90
|
+
return {};
|
91
|
+
}
|
92
|
+
|
93
|
+
return { provider: samlConfig.idpMetadata.provider };
|
94
|
+
} else {
|
95
|
+
const samlConfigs = await configStore.getByIndex({
|
96
|
+
name: indexNames.tenantProduct,
|
97
|
+
value: dbutils.keyFromParts(tenant, product),
|
98
|
+
});
|
99
|
+
if (!samlConfigs || !samlConfigs.length) {
|
100
|
+
return {};
|
101
|
+
}
|
102
|
+
|
103
|
+
return { provider: samlConfigs[0].idpMetadata.provider };
|
104
|
+
}
|
105
|
+
};
|
106
|
+
|
62
107
|
module.exports = (opts) => {
|
63
108
|
configStore = opts.configStore;
|
64
109
|
return {
|
65
110
|
config,
|
111
|
+
getConfig,
|
66
112
|
};
|
67
113
|
};
|
package/src/jackson.js
CHANGED
@@ -87,6 +87,22 @@ internalApp.post(apiPath + '/config', async (req, res) => {
|
|
87
87
|
}
|
88
88
|
});
|
89
89
|
|
90
|
+
internalApp.get(apiPath + '/config', 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
|
+
|
90
106
|
let internalServer = server;
|
91
107
|
if (env.useInternalServer) {
|
92
108
|
internalServer = internalApp.listen(env.internalHostPort, async () => {
|