@boxyhq/saml-jackson 0.2.4-beta.198 → 0.3.0-beta.248
Sign up to get free protection for your applications and to get access to all the features.
- package/Dockerfile +9 -7
- package/README.md +28 -29
- package/dist/controller/api.d.ts +32 -0
- package/dist/controller/api.js +193 -0
- package/dist/controller/error.d.ts +5 -0
- package/dist/controller/error.js +12 -0
- package/dist/controller/oauth/allowed.d.ts +1 -0
- package/dist/controller/oauth/allowed.js +17 -0
- package/dist/controller/oauth/code-verifier.d.ts +2 -0
- package/dist/controller/oauth/code-verifier.js +15 -0
- package/dist/controller/oauth/redirect.d.ts +1 -0
- package/dist/controller/oauth/redirect.js +11 -0
- package/dist/controller/oauth.d.ts +23 -0
- package/dist/controller/oauth.js +263 -0
- package/dist/controller/utils.d.ts +6 -0
- package/dist/controller/utils.js +17 -0
- package/dist/db/db.d.ts +15 -0
- package/dist/db/db.js +107 -0
- package/dist/db/encrypter.d.ts +3 -0
- package/dist/db/encrypter.js +29 -0
- package/dist/db/mem.d.ts +20 -0
- package/dist/db/mem.js +128 -0
- package/dist/db/mongo.d.ts +17 -0
- package/dist/db/mongo.js +106 -0
- package/dist/db/redis.d.ts +15 -0
- package/dist/db/redis.js +107 -0
- package/dist/db/sql/entity/JacksonIndex.d.ts +7 -0
- package/dist/db/sql/entity/JacksonIndex.js +41 -0
- package/dist/db/sql/entity/JacksonStore.d.ts +6 -0
- package/dist/db/sql/entity/JacksonStore.js +42 -0
- package/dist/db/sql/entity/JacksonTTL.d.ts +4 -0
- package/dist/db/sql/entity/JacksonTTL.js +29 -0
- package/dist/db/sql/sql.d.ts +20 -0
- package/dist/db/sql/sql.js +174 -0
- package/dist/db/store.d.ts +5 -0
- package/dist/db/store.js +68 -0
- package/dist/db/utils.d.ts +7 -0
- package/dist/db/utils.js +29 -0
- package/dist/env.d.ts +22 -0
- package/dist/env.js +35 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +82 -0
- package/dist/jackson.d.ts +1 -0
- package/dist/jackson.js +153 -0
- package/dist/read-config.d.ts +3 -0
- package/dist/read-config.js +50 -0
- package/dist/saml/claims.d.ts +6 -0
- package/dist/saml/claims.js +35 -0
- package/dist/saml/saml.d.ts +11 -0
- package/dist/saml/saml.js +200 -0
- package/dist/saml/x509.d.ts +7 -0
- package/dist/saml/x509.js +69 -0
- package/dist/typings.d.ts +137 -0
- package/dist/typings.js +2 -0
- package/package.json +41 -21
- package/.dockerignore +0 -2
- package/.eslintrc.js +0 -13
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
- package/.github/ISSUE_TEMPLATE/config.yml +0 -5
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -43
- package/.github/pull_request_template.md +0 -31
- package/.github/workflows/codesee-arch-diagram.yml +0 -81
- package/.github/workflows/main.yml +0 -123
- package/_dev/docker-compose.yml +0 -37
- package/map.js +0 -1
- package/prettier.config.js +0 -4
- package/src/controller/api.js +0 -167
- package/src/controller/error.js +0 -12
- package/src/controller/oauth/allowed.js +0 -19
- package/src/controller/oauth/code-verifier.js +0 -16
- package/src/controller/oauth/redirect.js +0 -18
- package/src/controller/oauth.js +0 -321
- package/src/controller/utils.js +0 -19
- package/src/db/db.js +0 -81
- package/src/db/db.test.js +0 -302
- package/src/db/encrypter.js +0 -36
- package/src/db/mem.js +0 -111
- package/src/db/mongo.js +0 -89
- package/src/db/redis.js +0 -88
- package/src/db/sql/entity/JacksonIndex.js +0 -42
- package/src/db/sql/entity/JacksonStore.js +0 -42
- package/src/db/sql/entity/JacksonTTL.js +0 -23
- package/src/db/sql/model/JacksonIndex.js +0 -9
- package/src/db/sql/model/JacksonStore.js +0 -10
- package/src/db/sql/model/JacksonTTL.js +0 -8
- package/src/db/sql/sql.js +0 -153
- package/src/db/store.js +0 -42
- package/src/db/utils.js +0 -30
- package/src/env.js +0 -39
- package/src/index.js +0 -67
- package/src/jackson.js +0 -161
- package/src/read-config.js +0 -24
- package/src/saml/claims.js +0 -40
- package/src/saml/saml.js +0 -223
- package/src/saml/x509.js +0 -48
- package/src/test/api.test.js +0 -186
- package/src/test/data/metadata/boxyhq.js +0 -6
- package/src/test/data/metadata/boxyhq.xml +0 -30
- package/src/test/data/saml_response +0 -1
- package/src/test/oauth.test.js +0 -342
package/Dockerfile
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# Install dependencies only when needed
|
2
|
-
FROM node:16.13.1-alpine3.14 AS
|
2
|
+
FROM node:16.13.1-alpine3.14 AS build
|
3
3
|
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
4
4
|
RUN apk add --no-cache libc6-compat
|
5
5
|
WORKDIR /app
|
6
6
|
COPY src/ src/
|
7
|
-
COPY package.json package-lock.json ./
|
8
|
-
RUN npm
|
7
|
+
COPY package.json package-lock.json tsconfig*.json ./
|
8
|
+
RUN npm install
|
9
|
+
RUN npm run build
|
9
10
|
|
10
11
|
# Production image, copy all the files and run next
|
11
12
|
FROM node:16.13.1-alpine3.14 AS runner
|
@@ -17,13 +18,14 @@ ENV NODE_ENV production
|
|
17
18
|
RUN addgroup -g 1001 -S nodejs
|
18
19
|
RUN adduser -S nodejs -u 1001
|
19
20
|
|
20
|
-
COPY --from=
|
21
|
-
COPY --from=
|
22
|
-
COPY --from=
|
21
|
+
COPY --from=build /app/dist ./dist
|
22
|
+
COPY --from=build /app/package.json ./package.json
|
23
|
+
COPY --from=build /app/package-lock.json ./package-lock.json
|
24
|
+
RUN npm ci --only=production
|
23
25
|
|
24
26
|
USER nodejs
|
25
27
|
|
26
28
|
EXPOSE 5000
|
27
29
|
EXPOSE 6000
|
28
30
|
|
29
|
-
CMD [ "node", "
|
31
|
+
CMD [ "node", "dist/jackson.js" ]
|
package/README.md
CHANGED
@@ -4,28 +4,28 @@ 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
9
|
[CodeSee codebase visualizer](https://app.codesee.io/maps/public/53e91640-23b5-11ec-a724-79d7dd589517)
|
10
10
|
|
11
|
-
|
11
|
+
## Getting Started
|
12
12
|
|
13
13
|
There are two ways to use this repo.
|
14
14
|
|
15
|
-
- As an npm library
|
15
|
+
- As an npm library
|
16
16
|
- As a separate service
|
17
17
|
|
18
18
|
## Install as an npm library
|
19
19
|
|
20
20
|
Jackson is available as an [npm package](https://www.npmjs.com/package/@boxyhq/saml-jackson) that can be integrated into any web application framework (like Express.js for example). Please file an issue or submit a PR if you encounter any issues with your choice of framework.
|
21
21
|
|
22
|
-
```
|
22
|
+
```bash
|
23
23
|
npm i @boxyhq/saml-jackson
|
24
24
|
```
|
25
25
|
|
26
26
|
### Add Express Routes
|
27
27
|
|
28
|
-
```
|
28
|
+
```javascript
|
29
29
|
// express
|
30
30
|
const express = require('express');
|
31
31
|
const router = express.Router();
|
@@ -169,11 +169,11 @@ app.use('/sso', router);
|
|
169
169
|
|
170
170
|
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.
|
171
171
|
|
172
|
-
```
|
172
|
+
```bash
|
173
173
|
docker run -p 5000:5000 -p 6000:6000 boxyhq/jackson:78e9099d
|
174
174
|
```
|
175
175
|
|
176
|
-
Refer to https://github.com/boxyhq/jackson#configuration for the full configuration.
|
176
|
+
Refer to <https://github.com/boxyhq/jackson#configuration> for the full configuration.
|
177
177
|
|
178
178
|
Kubernetes and docker-compose deployment files will be coming soon.
|
179
179
|
|
@@ -187,12 +187,12 @@ Please follow the instructions [here](https://docs.google.com/document/d/1fk---Z
|
|
187
187
|
|
188
188
|
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.
|
189
189
|
|
190
|
-
| SAML Attribute
|
191
|
-
|
|
192
|
-
| http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier | id |
|
193
|
-
| http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress | email |
|
194
|
-
| http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname | firstName |
|
195
|
-
| http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname | lastName |
|
190
|
+
| SAML Attribute | Jackson mapping |
|
191
|
+
| ---------------------------------------------------------------------- | --------------- |
|
192
|
+
| <http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier> | id |
|
193
|
+
| <http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress> | email |
|
194
|
+
| <http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname> | firstName |
|
195
|
+
| <http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname> | lastName |
|
196
196
|
|
197
197
|
### 2. SAML config API
|
198
198
|
|
@@ -202,7 +202,7 @@ You will need to provide a place in the UI for your customers (The account setti
|
|
202
202
|
|
203
203
|
The following API call sets up the configuration in Jackson:
|
204
204
|
|
205
|
-
```
|
205
|
+
```bash
|
206
206
|
curl --location --request POST 'http://localhost:6000/api/v1/saml/config' \
|
207
207
|
--header 'Authorization: Api-Key <Jackson API Key>' \
|
208
208
|
--header 'Content-Type: application/x-www-form-urlencoded' \
|
@@ -225,7 +225,7 @@ The response returns a JSON with `client_id` and `client_secret` that can be sto
|
|
225
225
|
|
226
226
|
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.
|
227
227
|
|
228
|
-
```
|
228
|
+
```bash
|
229
229
|
curl -G --location 'http://localhost:6000/api/v1/saml/config' \
|
230
230
|
--header 'Authorization: Api-Key <Jackson API Key>' \
|
231
231
|
--header 'Content-Type: application/x-www-form-urlencoded' \
|
@@ -233,7 +233,7 @@ curl -G --location 'http://localhost:6000/api/v1/saml/config' \
|
|
233
233
|
--data-urlencode 'product=demo'
|
234
234
|
```
|
235
235
|
|
236
|
-
```
|
236
|
+
```bash
|
237
237
|
curl -G --location 'http://localhost:6000/api/v1/saml/config' \
|
238
238
|
--header 'Authorization: Api-Key <Jackson API Key>' \
|
239
239
|
--header 'Content-Type: application/x-www-form-urlencoded' \
|
@@ -246,7 +246,7 @@ The response returns a JSON with `provider` indicating the domain of your Identi
|
|
246
246
|
|
247
247
|
This endpoint can be used to delete an existing IdP metadata.
|
248
248
|
|
249
|
-
```
|
249
|
+
```bash
|
250
250
|
curl -X "DELETE" --location 'http://localhost:6000/api/v1/saml/config' \
|
251
251
|
--header 'Authorization: Api-Key <Jackson API Key>' \
|
252
252
|
--header 'Content-Type: application/x-www-form-urlencoded' \
|
@@ -254,7 +254,7 @@ curl -X "DELETE" --location 'http://localhost:6000/api/v1/saml/config' \
|
|
254
254
|
--data-urlencode 'product=demo'
|
255
255
|
```
|
256
256
|
|
257
|
-
```
|
257
|
+
```bash
|
258
258
|
curl -X "DELETE" --location 'http://localhost:6000/api/v1/saml/config' \
|
259
259
|
--header 'Authorization: Api-Key <Jackson API Key>' \
|
260
260
|
--header 'Content-Type: application/x-www-form-urlencoded' \
|
@@ -266,7 +266,7 @@ curl -X "DELETE" --location 'http://localhost:6000/api/v1/saml/config' \
|
|
266
266
|
|
267
267
|
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.
|
268
268
|
|
269
|
-
Jackson also supports the PKCE authorization flow (https://oauth.net/2/pkce
|
269
|
+
Jackson also supports the PKCE authorization flow (<https://oauth.net/2/pkce/>), so you can protect your SPAs.
|
270
270
|
|
271
271
|
If for any reason you need to implement the flow on your own, the steps are outlined below:
|
272
272
|
|
@@ -274,7 +274,7 @@ If for any reason you need to implement the flow on your own, the steps are outl
|
|
274
274
|
|
275
275
|
The OAuth flow begins with redirecting your user to the `authorize` URL:
|
276
276
|
|
277
|
-
```
|
277
|
+
```bash
|
278
278
|
https://localhost:5000/oauth/authorize
|
279
279
|
?response_type=code&provider=saml
|
280
280
|
&client_id=<clientID or tenant and product query params as described in the SAML config API section above>
|
@@ -295,7 +295,7 @@ After successful authorization, the user is redirected back to the `redirect_uri
|
|
295
295
|
|
296
296
|
The code can then be exchanged for a token by making the following request:
|
297
297
|
|
298
|
-
```
|
298
|
+
```bash
|
299
299
|
curl --request POST \
|
300
300
|
--url 'http://localhost:5000/oauth/token' \
|
301
301
|
--header 'content-type: application/x-www-form-urlencoded' \
|
@@ -313,7 +313,7 @@ curl --request POST \
|
|
313
313
|
|
314
314
|
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.
|
315
315
|
|
316
|
-
```
|
316
|
+
```json
|
317
317
|
{
|
318
318
|
"access_token": <access token>,
|
319
319
|
"token_type": "bearer",
|
@@ -325,7 +325,7 @@ If everything goes well you should receive a JSON response that includes the acc
|
|
325
325
|
|
326
326
|
The short-lived access token can now be used to request the user's profile. You'll need to make the following request:
|
327
327
|
|
328
|
-
```
|
328
|
+
```bash
|
329
329
|
curl --request GET \
|
330
330
|
--url https://localhost:5000/oauth/userinfo \
|
331
331
|
--header 'authorization: Bearer <access token>' \
|
@@ -334,7 +334,7 @@ curl --request GET \
|
|
334
334
|
|
335
335
|
If everything goes well you should receive a JSON response with the user's profile:
|
336
336
|
|
337
|
-
```
|
337
|
+
```json
|
338
338
|
{
|
339
339
|
"id": <id from the Identity Provider>,
|
340
340
|
"email": "sjackson@coolstartup.com",
|
@@ -357,7 +357,6 @@ To Do
|
|
357
357
|
Jackson currently supports the following databases.
|
358
358
|
|
359
359
|
- Postgres
|
360
|
-
- CockroachDB
|
361
360
|
- MySQL
|
362
361
|
- MariaDB
|
363
362
|
- MongoDB
|
@@ -381,7 +380,7 @@ The following options are supported and will have to be configured during deploy
|
|
381
380
|
| 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` |
|
382
381
|
| DB_ENGINE (npm: db.engine) | Supported values are `redis`, `sql`, `mongo`, `mem`. | `sql` |
|
383
382
|
| DB_URL (npm: db.url) | The database URL to connect to. For example `postgres://postgres:postgres@localhost:5450/jackson` | |
|
384
|
-
| DB_TYPE (npm: db.type) | Only needed when DB_ENGINE is `sql`. Supported values are `postgres`, `
|
383
|
+
| DB_TYPE (npm: db.type) | Only needed when DB_ENGINE is `sql`. Supported values are `postgres`, `mysql`, `mariadb`. | `postgres` |
|
385
384
|
| DB_TTL (npm: db.ttl) | TTL for the code, session and token stores (in seconds). | 300 |
|
386
385
|
| DB_CLEANUP_LIMIT (npm: db.cleanupLimit) | Limit cleanup of TTL entries to this number. | 1000 |
|
387
386
|
| DB_ENCRYPTION_KEY (npm: db.encryptionKey) | To encrypt data at rest specify a 32 character key. | |
|
@@ -391,7 +390,7 @@ The following options are supported and will have to be configured during deploy
|
|
391
390
|
|
392
391
|
If PRE_LOADED_CONFIG is set then it should point to a directory with the following structure (example below):-
|
393
392
|
|
394
|
-
```
|
393
|
+
```bash
|
395
394
|
boxyhq.js
|
396
395
|
boxyhq.xml
|
397
396
|
anothertenant.js
|
@@ -400,7 +399,7 @@ anothertenant.xml
|
|
400
399
|
|
401
400
|
The JS file has the following structure:-
|
402
401
|
|
403
|
-
```
|
402
|
+
```javascript
|
404
403
|
module.exports = {
|
405
404
|
defaultRedirectUrl: 'http://localhost:3000/login/saml',
|
406
405
|
redirectUrl: '["http://localhost:3000/*", "http://localhost:5000/*"]',
|
@@ -415,7 +414,7 @@ The config and XML above correspond to the `SAML API config` (see below).
|
|
415
414
|
|
416
415
|
## SAML Login flows
|
417
416
|
|
418
|
-
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
|
417
|
+
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/>.
|
419
418
|
|
420
419
|
## Contributing
|
421
420
|
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import { IdPConfig, ISAMLConfig, OAuth } from '../typings';
|
2
|
+
export declare class SAMLConfig implements ISAMLConfig {
|
3
|
+
private configStore;
|
4
|
+
constructor({ configStore }: {
|
5
|
+
configStore: any;
|
6
|
+
});
|
7
|
+
private _validateIdPConfig;
|
8
|
+
create(body: IdPConfig): Promise<OAuth>;
|
9
|
+
get(body: {
|
10
|
+
clientID: string;
|
11
|
+
tenant: string;
|
12
|
+
product: string;
|
13
|
+
}): Promise<Partial<OAuth>>;
|
14
|
+
delete(body: {
|
15
|
+
clientID: string;
|
16
|
+
clientSecret: string;
|
17
|
+
tenant: string;
|
18
|
+
product: string;
|
19
|
+
}): Promise<void>;
|
20
|
+
config(body: IdPConfig): Promise<OAuth>;
|
21
|
+
getConfig(body: {
|
22
|
+
clientID: string;
|
23
|
+
tenant: string;
|
24
|
+
product: string;
|
25
|
+
}): Promise<Partial<OAuth>>;
|
26
|
+
deleteConfig(body: {
|
27
|
+
clientID: string;
|
28
|
+
clientSecret: string;
|
29
|
+
tenant: string;
|
30
|
+
product: string;
|
31
|
+
}): Promise<void>;
|
32
|
+
}
|
@@ -0,0 +1,193 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
5
|
+
}) : (function(o, m, k, k2) {
|
6
|
+
if (k2 === undefined) k2 = k;
|
7
|
+
o[k2] = m[k];
|
8
|
+
}));
|
9
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
10
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
11
|
+
}) : function(o, v) {
|
12
|
+
o["default"] = v;
|
13
|
+
});
|
14
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
15
|
+
if (mod && mod.__esModule) return mod;
|
16
|
+
var result = {};
|
17
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
18
|
+
__setModuleDefault(result, mod);
|
19
|
+
return result;
|
20
|
+
};
|
21
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
22
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
23
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
24
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
25
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
26
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
27
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
28
|
+
});
|
29
|
+
};
|
30
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
31
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
32
|
+
};
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
34
|
+
exports.SAMLConfig = void 0;
|
35
|
+
const crypto_1 = __importDefault(require("crypto"));
|
36
|
+
const dbutils = __importStar(require("../db/utils"));
|
37
|
+
const saml_1 = __importDefault(require("../saml/saml"));
|
38
|
+
const x509_1 = __importDefault(require("../saml/x509"));
|
39
|
+
const error_1 = require("./error");
|
40
|
+
const utils_1 = require("./utils");
|
41
|
+
class SAMLConfig {
|
42
|
+
constructor({ configStore }) {
|
43
|
+
this.configStore = configStore;
|
44
|
+
}
|
45
|
+
_validateIdPConfig(body) {
|
46
|
+
const { rawMetadata, defaultRedirectUrl, redirectUrl, tenant, product } = body;
|
47
|
+
if (!rawMetadata) {
|
48
|
+
throw new error_1.JacksonError('Please provide rawMetadata', 400);
|
49
|
+
}
|
50
|
+
if (!defaultRedirectUrl) {
|
51
|
+
throw new error_1.JacksonError('Please provide a defaultRedirectUrl', 400);
|
52
|
+
}
|
53
|
+
if (!redirectUrl) {
|
54
|
+
throw new error_1.JacksonError('Please provide redirectUrl', 400);
|
55
|
+
}
|
56
|
+
if (!tenant) {
|
57
|
+
throw new error_1.JacksonError('Please provide tenant', 400);
|
58
|
+
}
|
59
|
+
if (!product) {
|
60
|
+
throw new error_1.JacksonError('Please provide product', 400);
|
61
|
+
}
|
62
|
+
}
|
63
|
+
create(body) {
|
64
|
+
return __awaiter(this, void 0, void 0, function* () {
|
65
|
+
const { rawMetadata, defaultRedirectUrl, redirectUrl, tenant, product } = body;
|
66
|
+
this._validateIdPConfig(body);
|
67
|
+
const idpMetadata = yield saml_1.default.parseMetadataAsync(rawMetadata);
|
68
|
+
// extract provider
|
69
|
+
let providerName = extractHostName(idpMetadata.entityID);
|
70
|
+
if (!providerName) {
|
71
|
+
providerName = extractHostName(idpMetadata.sso.redirectUrl || idpMetadata.sso.postUrl);
|
72
|
+
}
|
73
|
+
idpMetadata.provider = providerName ? providerName : 'Unknown';
|
74
|
+
const clientID = dbutils.keyDigest(dbutils.keyFromParts(tenant, product, idpMetadata.entityID));
|
75
|
+
let clientSecret;
|
76
|
+
const exists = yield this.configStore.get(clientID);
|
77
|
+
if (exists) {
|
78
|
+
clientSecret = exists.clientSecret;
|
79
|
+
}
|
80
|
+
else {
|
81
|
+
clientSecret = crypto_1.default.randomBytes(24).toString('hex');
|
82
|
+
}
|
83
|
+
const certs = yield x509_1.default.generate();
|
84
|
+
if (!certs) {
|
85
|
+
throw new Error('Error generating x59 certs');
|
86
|
+
}
|
87
|
+
yield this.configStore.put(clientID, {
|
88
|
+
idpMetadata,
|
89
|
+
defaultRedirectUrl,
|
90
|
+
redirectUrl: JSON.parse(redirectUrl),
|
91
|
+
tenant,
|
92
|
+
product,
|
93
|
+
clientID,
|
94
|
+
clientSecret,
|
95
|
+
certs,
|
96
|
+
}, {
|
97
|
+
// secondary index on entityID
|
98
|
+
name: utils_1.IndexNames.EntityID,
|
99
|
+
value: idpMetadata.entityID,
|
100
|
+
}, {
|
101
|
+
// secondary index on tenant + product
|
102
|
+
name: utils_1.IndexNames.TenantProduct,
|
103
|
+
value: dbutils.keyFromParts(tenant, product),
|
104
|
+
});
|
105
|
+
return {
|
106
|
+
client_id: clientID,
|
107
|
+
client_secret: clientSecret,
|
108
|
+
provider: idpMetadata.provider,
|
109
|
+
};
|
110
|
+
});
|
111
|
+
}
|
112
|
+
get(body) {
|
113
|
+
return __awaiter(this, void 0, void 0, function* () {
|
114
|
+
const { clientID, tenant, product } = body;
|
115
|
+
if (clientID) {
|
116
|
+
const samlConfig = yield this.configStore.get(clientID);
|
117
|
+
return samlConfig ? { provider: samlConfig.idpMetadata.provider } : {};
|
118
|
+
}
|
119
|
+
if (tenant && product) {
|
120
|
+
const samlConfigs = yield this.configStore.getByIndex({
|
121
|
+
name: utils_1.IndexNames.TenantProduct,
|
122
|
+
value: dbutils.keyFromParts(tenant, product),
|
123
|
+
});
|
124
|
+
if (!samlConfigs || !samlConfigs.length) {
|
125
|
+
return {};
|
126
|
+
}
|
127
|
+
return { provider: samlConfigs[0].idpMetadata.provider };
|
128
|
+
}
|
129
|
+
throw new error_1.JacksonError('Please provide `clientID` or `tenant` and `product`.', 400);
|
130
|
+
});
|
131
|
+
}
|
132
|
+
delete(body) {
|
133
|
+
return __awaiter(this, void 0, void 0, function* () {
|
134
|
+
const { clientID, clientSecret, tenant, product } = body;
|
135
|
+
if (clientID && clientSecret) {
|
136
|
+
const samlConfig = yield this.configStore.get(clientID);
|
137
|
+
if (!samlConfig) {
|
138
|
+
return;
|
139
|
+
}
|
140
|
+
if (samlConfig.clientSecret === clientSecret) {
|
141
|
+
yield this.configStore.delete(clientID);
|
142
|
+
}
|
143
|
+
else {
|
144
|
+
throw new error_1.JacksonError('clientSecret mismatch.', 400);
|
145
|
+
}
|
146
|
+
return;
|
147
|
+
}
|
148
|
+
if (tenant && product) {
|
149
|
+
const samlConfigs = yield this.configStore.getByIndex({
|
150
|
+
name: utils_1.IndexNames.TenantProduct,
|
151
|
+
value: dbutils.keyFromParts(tenant, product),
|
152
|
+
});
|
153
|
+
if (!samlConfigs || !samlConfigs.length) {
|
154
|
+
return;
|
155
|
+
}
|
156
|
+
for (const conf of samlConfigs) {
|
157
|
+
yield this.configStore.delete(conf.clientID);
|
158
|
+
}
|
159
|
+
return;
|
160
|
+
}
|
161
|
+
throw new error_1.JacksonError('Please provide `clientID` and `clientSecret` or `tenant` and `product`.', 400);
|
162
|
+
});
|
163
|
+
}
|
164
|
+
// Ensure backward compatibility
|
165
|
+
config(body) {
|
166
|
+
return __awaiter(this, void 0, void 0, function* () {
|
167
|
+
return this.create(body);
|
168
|
+
});
|
169
|
+
}
|
170
|
+
getConfig(body) {
|
171
|
+
return __awaiter(this, void 0, void 0, function* () {
|
172
|
+
return this.get(body);
|
173
|
+
});
|
174
|
+
}
|
175
|
+
deleteConfig(body) {
|
176
|
+
return __awaiter(this, void 0, void 0, function* () {
|
177
|
+
return this.delete(body);
|
178
|
+
});
|
179
|
+
}
|
180
|
+
}
|
181
|
+
exports.SAMLConfig = SAMLConfig;
|
182
|
+
const extractHostName = (url) => {
|
183
|
+
try {
|
184
|
+
const pUrl = new URL(url);
|
185
|
+
if (pUrl.hostname.startsWith('www.')) {
|
186
|
+
return pUrl.hostname.substring(4);
|
187
|
+
}
|
188
|
+
return pUrl.hostname;
|
189
|
+
}
|
190
|
+
catch (err) {
|
191
|
+
return null;
|
192
|
+
}
|
193
|
+
};
|
@@ -0,0 +1,12 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.JacksonError = void 0;
|
4
|
+
class JacksonError extends Error {
|
5
|
+
constructor(message, statusCode = 500) {
|
6
|
+
super(message);
|
7
|
+
this.name = this.constructor.name;
|
8
|
+
this.statusCode = statusCode;
|
9
|
+
Error.captureStackTrace(this, this.constructor);
|
10
|
+
}
|
11
|
+
}
|
12
|
+
exports.JacksonError = JacksonError;
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare const redirect: (redirectUrl: string, redirectUrls: string[]) => boolean;
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.redirect = void 0;
|
4
|
+
const redirect = (redirectUrl, redirectUrls) => {
|
5
|
+
const url = new URL(redirectUrl);
|
6
|
+
for (const idx in redirectUrls) {
|
7
|
+
const rUrl = new URL(redirectUrls[idx]);
|
8
|
+
// TODO: Check pathname, for now pathname is ignored
|
9
|
+
if (rUrl.protocol === url.protocol &&
|
10
|
+
rUrl.hostname === url.hostname &&
|
11
|
+
rUrl.port === url.port) {
|
12
|
+
return true;
|
13
|
+
}
|
14
|
+
}
|
15
|
+
return false;
|
16
|
+
};
|
17
|
+
exports.redirect = redirect;
|
@@ -0,0 +1,15 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.encode = exports.transformBase64 = void 0;
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
8
|
+
const transformBase64 = (input) => {
|
9
|
+
return input.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
10
|
+
};
|
11
|
+
exports.transformBase64 = transformBase64;
|
12
|
+
const encode = (code_challenge) => {
|
13
|
+
return (0, exports.transformBase64)(crypto_1.default.createHash('sha256').update(code_challenge).digest('base64'));
|
14
|
+
};
|
15
|
+
exports.encode = encode;
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare const success: (redirectUrl: string, params: Record<string, string>) => string;
|
@@ -0,0 +1,11 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.success = void 0;
|
4
|
+
const success = (redirectUrl, params) => {
|
5
|
+
const url = new URL(redirectUrl);
|
6
|
+
for (const [key, value] of Object.entries(params)) {
|
7
|
+
url.searchParams.set(key, value);
|
8
|
+
}
|
9
|
+
return url.href;
|
10
|
+
};
|
11
|
+
exports.success = success;
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { IOAuthController, OAuthReqBody, OAuthTokenReq, OAuthTokenRes, Profile, SAMLResponsePayload } from '../typings';
|
2
|
+
export declare class OAuthController implements IOAuthController {
|
3
|
+
private configStore;
|
4
|
+
private sessionStore;
|
5
|
+
private codeStore;
|
6
|
+
private tokenStore;
|
7
|
+
private opts;
|
8
|
+
constructor({ configStore, sessionStore, codeStore, tokenStore, opts }: {
|
9
|
+
configStore: any;
|
10
|
+
sessionStore: any;
|
11
|
+
codeStore: any;
|
12
|
+
tokenStore: any;
|
13
|
+
opts: any;
|
14
|
+
});
|
15
|
+
authorize(body: OAuthReqBody): Promise<{
|
16
|
+
redirect_url: string;
|
17
|
+
}>;
|
18
|
+
samlResponse(body: SAMLResponsePayload): Promise<{
|
19
|
+
redirect_url: string;
|
20
|
+
}>;
|
21
|
+
token(body: OAuthTokenReq): Promise<OAuthTokenRes>;
|
22
|
+
userInfo(token: string): Promise<Profile>;
|
23
|
+
}
|