@boxyhq/saml-jackson 0.2.4 → 0.3.0-beta.246

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.
Files changed (100) hide show
  1. package/Dockerfile +9 -7
  2. package/README.md +1 -2
  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 +80 -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
@@ -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. | |
@@ -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
+ }
@@ -0,0 +1,263 @@
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.OAuthController = 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 error_1 = require("./error");
39
+ const allowed = __importStar(require("./oauth/allowed"));
40
+ const codeVerifier = __importStar(require("./oauth/code-verifier"));
41
+ const redirect = __importStar(require("./oauth/redirect"));
42
+ const utils_1 = require("./utils");
43
+ const relayStatePrefix = 'boxyhq_jackson_';
44
+ function getEncodedClientId(client_id) {
45
+ try {
46
+ const sp = new URLSearchParams(client_id);
47
+ const tenant = sp.get('tenant');
48
+ const product = sp.get('product');
49
+ if (tenant && product) {
50
+ return {
51
+ tenant: sp.get('tenant'),
52
+ product: sp.get('product'),
53
+ };
54
+ }
55
+ return null;
56
+ }
57
+ catch (err) {
58
+ return null;
59
+ }
60
+ }
61
+ class OAuthController {
62
+ constructor({ configStore, sessionStore, codeStore, tokenStore, opts }) {
63
+ this.configStore = configStore;
64
+ this.sessionStore = sessionStore;
65
+ this.codeStore = codeStore;
66
+ this.tokenStore = tokenStore;
67
+ this.opts = opts;
68
+ }
69
+ authorize(body) {
70
+ return __awaiter(this, void 0, void 0, function* () {
71
+ const { response_type = 'code', client_id, redirect_uri, state, tenant, product, code_challenge, code_challenge_method = '',
72
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
73
+ provider = 'saml', } = body;
74
+ if (!redirect_uri) {
75
+ throw new error_1.JacksonError('Please specify a redirect URL.', 400);
76
+ }
77
+ if (!state) {
78
+ throw new error_1.JacksonError('Please specify a state to safeguard against XSRF attacks.', 400);
79
+ }
80
+ let samlConfig;
81
+ if (tenant && product) {
82
+ const samlConfigs = yield this.configStore.getByIndex({
83
+ name: utils_1.IndexNames.TenantProduct,
84
+ value: dbutils.keyFromParts(tenant, product),
85
+ });
86
+ if (!samlConfigs || samlConfigs.length === 0) {
87
+ throw new error_1.JacksonError('SAML configuration not found.', 403);
88
+ }
89
+ // TODO: Support multiple matches
90
+ samlConfig = samlConfigs[0];
91
+ }
92
+ else if (client_id &&
93
+ client_id !== '' &&
94
+ client_id !== 'undefined' &&
95
+ client_id !== 'null') {
96
+ // if tenant and product are encoded in the client_id then we parse it and check for the relevant config(s)
97
+ const sp = getEncodedClientId(client_id);
98
+ if (sp === null || sp === void 0 ? void 0 : sp.tenant) {
99
+ const samlConfigs = yield this.configStore.getByIndex({
100
+ name: utils_1.IndexNames.TenantProduct,
101
+ value: dbutils.keyFromParts(sp.tenant, sp.product || ''),
102
+ });
103
+ if (!samlConfigs || samlConfigs.length === 0) {
104
+ throw new error_1.JacksonError('SAML configuration not found.', 403);
105
+ }
106
+ // TODO: Support multiple matches
107
+ samlConfig = samlConfigs[0];
108
+ }
109
+ else {
110
+ samlConfig = yield this.configStore.get(client_id);
111
+ }
112
+ }
113
+ else {
114
+ throw new error_1.JacksonError('You need to specify client_id or tenant & product', 403);
115
+ }
116
+ if (!samlConfig) {
117
+ throw new error_1.JacksonError('SAML configuration not found.', 403);
118
+ }
119
+ if (!allowed.redirect(redirect_uri, samlConfig.redirectUrl)) {
120
+ throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
121
+ }
122
+ const samlReq = saml_1.default.request({
123
+ entityID: this.opts.samlAudience,
124
+ callbackUrl: this.opts.externalUrl + this.opts.samlPath,
125
+ signingKey: samlConfig.certs.privateKey,
126
+ });
127
+ const sessionId = crypto_1.default.randomBytes(16).toString('hex');
128
+ yield this.sessionStore.put(sessionId, {
129
+ id: samlReq.id,
130
+ redirect_uri,
131
+ response_type,
132
+ state,
133
+ code_challenge,
134
+ code_challenge_method,
135
+ });
136
+ const redirectUrl = redirect.success(samlConfig.idpMetadata.sso.redirectUrl, {
137
+ RelayState: relayStatePrefix + sessionId,
138
+ SAMLRequest: Buffer.from(samlReq.request).toString('base64'),
139
+ });
140
+ return { redirect_url: redirectUrl };
141
+ });
142
+ }
143
+ samlResponse(body) {
144
+ return __awaiter(this, void 0, void 0, function* () {
145
+ const { SAMLResponse } = body; // RelayState will contain the sessionId from earlier quasi-oauth flow
146
+ let RelayState = body.RelayState || '';
147
+ if (!this.opts.idpEnabled && !RelayState.startsWith(relayStatePrefix)) {
148
+ // IDP is disabled so block the request
149
+ throw new error_1.JacksonError('IdP (Identity Provider) flow has been disabled. Please head to your Service Provider to login.', 403);
150
+ }
151
+ if (!RelayState.startsWith(relayStatePrefix)) {
152
+ RelayState = '';
153
+ }
154
+ RelayState = RelayState.replace(relayStatePrefix, '');
155
+ const rawResponse = Buffer.from(SAMLResponse, 'base64').toString();
156
+ const parsedResp = yield saml_1.default.parseAsync(rawResponse);
157
+ const samlConfigs = yield this.configStore.getByIndex({
158
+ name: utils_1.IndexNames.EntityID,
159
+ value: parsedResp === null || parsedResp === void 0 ? void 0 : parsedResp.issuer,
160
+ });
161
+ if (!samlConfigs || samlConfigs.length === 0) {
162
+ throw new error_1.JacksonError('SAML configuration not found.', 403);
163
+ }
164
+ // TODO: Support multiple matches
165
+ const samlConfig = samlConfigs[0];
166
+ let session;
167
+ if (RelayState !== '') {
168
+ session = yield this.sessionStore.get(RelayState);
169
+ if (!session) {
170
+ throw new error_1.JacksonError('Unable to validate state from the origin request.', 403);
171
+ }
172
+ }
173
+ const validateOpts = {
174
+ thumbprint: samlConfig.idpMetadata.thumbprint,
175
+ audience: this.opts.samlAudience,
176
+ };
177
+ if (session && session.id) {
178
+ validateOpts.inResponseTo = session.id;
179
+ }
180
+ const profile = yield saml_1.default.validateAsync(rawResponse, validateOpts);
181
+ // store details against a code
182
+ const code = crypto_1.default.randomBytes(20).toString('hex');
183
+ const codeVal = {
184
+ profile,
185
+ clientID: samlConfig.clientID,
186
+ clientSecret: samlConfig.clientSecret,
187
+ };
188
+ if (session) {
189
+ codeVal.session = session;
190
+ }
191
+ yield this.codeStore.put(code, codeVal);
192
+ if (session &&
193
+ session.redirect_uri &&
194
+ !allowed.redirect(session.redirect_uri, samlConfig.redirectUrl)) {
195
+ throw new error_1.JacksonError('Redirect URL is not allowed.', 403);
196
+ }
197
+ const params = {
198
+ code,
199
+ };
200
+ if (session && session.state) {
201
+ params.state = session.state;
202
+ }
203
+ const redirectUrl = redirect.success((session && session.redirect_uri) || samlConfig.defaultRedirectUrl, params);
204
+ return { redirect_url: redirectUrl };
205
+ });
206
+ }
207
+ token(body) {
208
+ return __awaiter(this, void 0, void 0, function* () {
209
+ const { client_id, client_secret, code_verifier, code, grant_type = 'authorization_code', } = body;
210
+ if (grant_type !== 'authorization_code') {
211
+ throw new error_1.JacksonError('Unsupported grant_type', 400);
212
+ }
213
+ if (!code) {
214
+ throw new error_1.JacksonError('Please specify code', 400);
215
+ }
216
+ const codeVal = yield this.codeStore.get(code);
217
+ if (!codeVal || !codeVal.profile) {
218
+ throw new error_1.JacksonError('Invalid code', 403);
219
+ }
220
+ if (client_id && client_secret) {
221
+ // check if we have an encoded client_id
222
+ if (client_id !== 'dummy' && client_secret !== 'dummy') {
223
+ const sp = getEncodedClientId(client_id);
224
+ if (!sp) {
225
+ // OAuth flow
226
+ if (client_id !== codeVal.clientID ||
227
+ client_secret !== codeVal.clientSecret) {
228
+ throw new error_1.JacksonError('Invalid client_id or client_secret', 401);
229
+ }
230
+ }
231
+ }
232
+ }
233
+ else if (code_verifier) {
234
+ // PKCE flow
235
+ let cv = code_verifier;
236
+ if (codeVal.session.code_challenge_method.toLowerCase() === 's256') {
237
+ cv = codeVerifier.encode(code_verifier);
238
+ }
239
+ if (codeVal.session.code_challenge !== cv) {
240
+ throw new error_1.JacksonError('Invalid code_verifier', 401);
241
+ }
242
+ }
243
+ else if (codeVal && codeVal.session) {
244
+ throw new error_1.JacksonError('Please specify client_secret or code_verifier', 401);
245
+ }
246
+ // store details against a token
247
+ const token = crypto_1.default.randomBytes(20).toString('hex');
248
+ yield this.tokenStore.put(token, codeVal.profile);
249
+ return {
250
+ access_token: token,
251
+ token_type: 'bearer',
252
+ expires_in: this.opts.db.ttl,
253
+ };
254
+ });
255
+ }
256
+ userInfo(token) {
257
+ return __awaiter(this, void 0, void 0, function* () {
258
+ const { claims } = yield this.tokenStore.get(token);
259
+ return claims;
260
+ });
261
+ }
262
+ }
263
+ exports.OAuthController = OAuthController;
@@ -0,0 +1,6 @@
1
+ import { Request } from 'express';
2
+ export declare const extractAuthToken: (req: Request) => string | null;
3
+ export declare enum IndexNames {
4
+ EntityID = "entityID",
5
+ TenantProduct = "tenantProduct"
6
+ }