@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.
Files changed (100) hide show
  1. package/Dockerfile +9 -7
  2. package/README.md +28 -29
  3. package/dist/controller/api.d.ts +32 -0
  4. package/dist/controller/api.js +193 -0
  5. package/dist/controller/error.d.ts +5 -0
  6. package/dist/controller/error.js +12 -0
  7. package/dist/controller/oauth/allowed.d.ts +1 -0
  8. package/dist/controller/oauth/allowed.js +17 -0
  9. package/dist/controller/oauth/code-verifier.d.ts +2 -0
  10. package/dist/controller/oauth/code-verifier.js +15 -0
  11. package/dist/controller/oauth/redirect.d.ts +1 -0
  12. package/dist/controller/oauth/redirect.js +11 -0
  13. package/dist/controller/oauth.d.ts +23 -0
  14. package/dist/controller/oauth.js +263 -0
  15. package/dist/controller/utils.d.ts +6 -0
  16. package/dist/controller/utils.js +17 -0
  17. package/dist/db/db.d.ts +15 -0
  18. package/dist/db/db.js +107 -0
  19. package/dist/db/encrypter.d.ts +3 -0
  20. package/dist/db/encrypter.js +29 -0
  21. package/dist/db/mem.d.ts +20 -0
  22. package/dist/db/mem.js +128 -0
  23. package/dist/db/mongo.d.ts +17 -0
  24. package/dist/db/mongo.js +106 -0
  25. package/dist/db/redis.d.ts +15 -0
  26. package/dist/db/redis.js +107 -0
  27. package/dist/db/sql/entity/JacksonIndex.d.ts +7 -0
  28. package/dist/db/sql/entity/JacksonIndex.js +41 -0
  29. package/dist/db/sql/entity/JacksonStore.d.ts +6 -0
  30. package/dist/db/sql/entity/JacksonStore.js +42 -0
  31. package/dist/db/sql/entity/JacksonTTL.d.ts +4 -0
  32. package/dist/db/sql/entity/JacksonTTL.js +29 -0
  33. package/dist/db/sql/sql.d.ts +20 -0
  34. package/dist/db/sql/sql.js +174 -0
  35. package/dist/db/store.d.ts +5 -0
  36. package/dist/db/store.js +68 -0
  37. package/dist/db/utils.d.ts +7 -0
  38. package/dist/db/utils.js +29 -0
  39. package/dist/env.d.ts +22 -0
  40. package/dist/env.js +35 -0
  41. package/dist/index.d.ts +9 -0
  42. package/dist/index.js +82 -0
  43. package/dist/jackson.d.ts +1 -0
  44. package/dist/jackson.js +153 -0
  45. package/dist/read-config.d.ts +3 -0
  46. package/dist/read-config.js +50 -0
  47. package/dist/saml/claims.d.ts +6 -0
  48. package/dist/saml/claims.js +35 -0
  49. package/dist/saml/saml.d.ts +11 -0
  50. package/dist/saml/saml.js +200 -0
  51. package/dist/saml/x509.d.ts +7 -0
  52. package/dist/saml/x509.js +69 -0
  53. package/dist/typings.d.ts +137 -0
  54. package/dist/typings.js +2 -0
  55. package/package.json +41 -21
  56. package/.dockerignore +0 -2
  57. package/.eslintrc.js +0 -13
  58. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  59. package/.github/ISSUE_TEMPLATE/config.yml +0 -5
  60. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -43
  61. package/.github/pull_request_template.md +0 -31
  62. package/.github/workflows/codesee-arch-diagram.yml +0 -81
  63. package/.github/workflows/main.yml +0 -123
  64. package/_dev/docker-compose.yml +0 -37
  65. package/map.js +0 -1
  66. package/prettier.config.js +0 -4
  67. package/src/controller/api.js +0 -167
  68. package/src/controller/error.js +0 -12
  69. package/src/controller/oauth/allowed.js +0 -19
  70. package/src/controller/oauth/code-verifier.js +0 -16
  71. package/src/controller/oauth/redirect.js +0 -18
  72. package/src/controller/oauth.js +0 -321
  73. package/src/controller/utils.js +0 -19
  74. package/src/db/db.js +0 -81
  75. package/src/db/db.test.js +0 -302
  76. package/src/db/encrypter.js +0 -36
  77. package/src/db/mem.js +0 -111
  78. package/src/db/mongo.js +0 -89
  79. package/src/db/redis.js +0 -88
  80. package/src/db/sql/entity/JacksonIndex.js +0 -42
  81. package/src/db/sql/entity/JacksonStore.js +0 -42
  82. package/src/db/sql/entity/JacksonTTL.js +0 -23
  83. package/src/db/sql/model/JacksonIndex.js +0 -9
  84. package/src/db/sql/model/JacksonStore.js +0 -10
  85. package/src/db/sql/model/JacksonTTL.js +0 -8
  86. package/src/db/sql/sql.js +0 -153
  87. package/src/db/store.js +0 -42
  88. package/src/db/utils.js +0 -30
  89. package/src/env.js +0 -39
  90. package/src/index.js +0 -67
  91. package/src/jackson.js +0 -161
  92. package/src/read-config.js +0 -24
  93. package/src/saml/claims.js +0 -40
  94. package/src/saml/saml.js +0 -223
  95. package/src/saml/x509.js +0 -48
  96. package/src/test/api.test.js +0 -186
  97. package/src/test/data/metadata/boxyhq.js +0 -6
  98. package/src/test/data/metadata/boxyhq.xml +0 -30
  99. package/src/test/data/saml_response +0 -1
  100. 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 deps
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 ci --only=production
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=deps /app/src ./src
21
- COPY --from=deps /app/node_modules ./node_modules
22
- COPY --from=deps /app/package.json ./package.json
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", "src/jackson.js" ]
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
- # Source code visualizer
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
- # Getting Started
11
+ ## Getting Started
12
12
 
13
13
  There are two ways to use this repo.
14
14
 
15
- - As an npm library (for Express compatible frameworks)
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 | 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 |
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/), so you can protect your SPAs.
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`, `cockroachdb`, `mysql`, `mariadb`. | `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,5 @@
1
+ export declare class JacksonError extends Error {
2
+ name: string;
3
+ statusCode: number;
4
+ constructor(message: string, statusCode?: number);
5
+ }
@@ -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,2 @@
1
+ export declare const transformBase64: (input: string) => string;
2
+ export declare const encode: (code_challenge: string) => string;
@@ -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
+ }