@boxyhq/saml-jackson 0.2.3-beta.231 → 0.2.3-beta.235

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 (50) hide show
  1. package/README.md +1 -2
  2. package/package.json +12 -4
  3. package/ nodemon.json +0 -12
  4. package/.dockerignore +0 -2
  5. package/.eslintrc.js +0 -18
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  7. package/.github/ISSUE_TEMPLATE/config.yml +0 -5
  8. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -43
  9. package/.github/pull_request_template.md +0 -31
  10. package/.github/workflows/codesee-arch-diagram.yml +0 -81
  11. package/.github/workflows/main.yml +0 -123
  12. package/_dev/docker-compose.yml +0 -37
  13. package/map.js +0 -1
  14. package/prettier.config.js +0 -4
  15. package/src/controller/api.ts +0 -225
  16. package/src/controller/error.ts +0 -13
  17. package/src/controller/oauth/allowed.ts +0 -22
  18. package/src/controller/oauth/code-verifier.ts +0 -11
  19. package/src/controller/oauth/redirect.ts +0 -12
  20. package/src/controller/oauth.ts +0 -334
  21. package/src/controller/utils.ts +0 -17
  22. package/src/db/db.ts +0 -100
  23. package/src/db/encrypter.ts +0 -38
  24. package/src/db/mem.ts +0 -128
  25. package/src/db/mongo.ts +0 -110
  26. package/src/db/redis.ts +0 -103
  27. package/src/db/sql/entity/JacksonIndex.ts +0 -43
  28. package/src/db/sql/entity/JacksonStore.ts +0 -43
  29. package/src/db/sql/entity/JacksonTTL.ts +0 -17
  30. package/src/db/sql/model/JacksonIndex.ts +0 -3
  31. package/src/db/sql/model/JacksonStore.ts +0 -8
  32. package/src/db/sql/sql.ts +0 -181
  33. package/src/db/store.ts +0 -49
  34. package/src/db/utils.ts +0 -26
  35. package/src/env.ts +0 -42
  36. package/src/index.ts +0 -84
  37. package/src/jackson.ts +0 -173
  38. package/src/read-config.ts +0 -29
  39. package/src/saml/claims.ts +0 -41
  40. package/src/saml/saml.ts +0 -233
  41. package/src/saml/x509.ts +0 -51
  42. package/src/test/api.test.ts +0 -270
  43. package/src/test/data/metadata/boxyhq.js +0 -6
  44. package/src/test/data/metadata/boxyhq.xml +0 -30
  45. package/src/test/data/saml_response +0 -1
  46. package/src/test/db.test.ts +0 -313
  47. package/src/test/oauth.test.ts +0 -362
  48. package/src/typings.ts +0 -167
  49. package/tsconfig.build.json +0 -6
  50. package/tsconfig.json +0 -26
@@ -1,13 +0,0 @@
1
- export class JacksonError extends Error {
2
- public name: string;
3
- public statusCode: number;
4
-
5
- constructor(message: string, statusCode = 500) {
6
- super(message);
7
-
8
- this.name = this.constructor.name;
9
- this.statusCode = statusCode;
10
-
11
- Error.captureStackTrace(this, this.constructor);
12
- }
13
- }
@@ -1,22 +0,0 @@
1
- export const redirect = (
2
- redirectUrl: string,
3
- redirectUrls: string[]
4
- ): boolean => {
5
- const url: URL = new URL(redirectUrl);
6
-
7
- for (const idx in redirectUrls) {
8
- const rUrl: URL = new URL(redirectUrls[idx]);
9
-
10
- // TODO: Check pathname, for now pathname is ignored
11
-
12
- if (
13
- rUrl.protocol === url.protocol &&
14
- rUrl.hostname === url.hostname &&
15
- rUrl.port === url.port
16
- ) {
17
- return true;
18
- }
19
- }
20
-
21
- return false;
22
- };
@@ -1,11 +0,0 @@
1
- import crypto from 'crypto';
2
-
3
- export const transformBase64 = (input: string): string => {
4
- return input.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
5
- };
6
-
7
- export const encode = (code_challenge: string): string => {
8
- return transformBase64(
9
- crypto.createHash('sha256').update(code_challenge).digest('base64')
10
- );
11
- };
@@ -1,12 +0,0 @@
1
- export const success = (
2
- redirectUrl: string,
3
- params: Record<string, string>
4
- ): string => {
5
- const url: URL = new URL(redirectUrl);
6
-
7
- for (const [key, value] of Object.entries(params)) {
8
- url.searchParams.set(key, value);
9
- }
10
-
11
- return url.href;
12
- };
@@ -1,334 +0,0 @@
1
- import crypto from 'crypto';
2
- import {
3
- IOAuthController,
4
- JacksonOption,
5
- OAuthReqBody,
6
- OAuthTokenReq,
7
- OAuthTokenRes,
8
- Profile,
9
- SAMLResponsePayload,
10
- Storable,
11
- } from 'saml-jackson';
12
- import * as dbutils from '../db/utils';
13
- import saml from '../saml/saml';
14
- import { JacksonError } from './error';
15
- import * as allowed from './oauth/allowed';
16
- import * as codeVerifier from './oauth/code-verifier';
17
- import * as redirect from './oauth/redirect';
18
- import { IndexNames } from './utils';
19
-
20
- const relayStatePrefix = 'boxyhq_jackson_';
21
-
22
- function getEncodedClientId(
23
- client_id: string
24
- ): { tenant: string | null; product: string | null } | null {
25
- try {
26
- const sp = new URLSearchParams(client_id);
27
- const tenant = sp.get('tenant');
28
- const product = sp.get('product');
29
- if (tenant && product) {
30
- return {
31
- tenant: sp.get('tenant'),
32
- product: sp.get('product'),
33
- };
34
- }
35
-
36
- return null;
37
- } catch (err) {
38
- return null;
39
- }
40
- }
41
-
42
- export class OAuthController implements IOAuthController {
43
- private configStore: Storable;
44
- private sessionStore: Storable;
45
- private codeStore: Storable;
46
- private tokenStore: Storable;
47
- private opts: JacksonOption;
48
-
49
- constructor({ configStore, sessionStore, codeStore, tokenStore, opts }) {
50
- this.configStore = configStore;
51
- this.sessionStore = sessionStore;
52
- this.codeStore = codeStore;
53
- this.tokenStore = tokenStore;
54
- this.opts = opts;
55
- }
56
-
57
- public async authorize(
58
- body: OAuthReqBody
59
- ): Promise<{ redirect_url: string }> {
60
- const {
61
- response_type = 'code',
62
- client_id,
63
- redirect_uri,
64
- state,
65
- tenant,
66
- product,
67
- code_challenge,
68
- code_challenge_method = '',
69
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
70
- provider = 'saml',
71
- } = body;
72
-
73
- if (!redirect_uri) {
74
- throw new JacksonError('Please specify a redirect URL.', 400);
75
- }
76
-
77
- if (!state) {
78
- throw new JacksonError(
79
- 'Please specify a state to safeguard against XSRF attacks.',
80
- 400
81
- );
82
- }
83
-
84
- let samlConfig;
85
-
86
- if (tenant && product) {
87
- const samlConfigs = await this.configStore.getByIndex({
88
- name: IndexNames.TenantProduct,
89
- value: dbutils.keyFromParts(tenant, product),
90
- });
91
-
92
- if (!samlConfigs || samlConfigs.length === 0) {
93
- throw new JacksonError('SAML configuration not found.', 403);
94
- }
95
-
96
- // TODO: Support multiple matches
97
- samlConfig = samlConfigs[0];
98
- } else if (
99
- client_id &&
100
- client_id !== '' &&
101
- client_id !== 'undefined' &&
102
- client_id !== 'null'
103
- ) {
104
- // if tenant and product are encoded in the client_id then we parse it and check for the relevant config(s)
105
- const sp = getEncodedClientId(client_id);
106
- if (sp?.tenant) {
107
- const samlConfigs = await this.configStore.getByIndex({
108
- name: IndexNames.TenantProduct,
109
- value: dbutils.keyFromParts(sp.tenant, sp.product || ''),
110
- });
111
-
112
- if (!samlConfigs || samlConfigs.length === 0) {
113
- throw new JacksonError('SAML configuration not found.', 403);
114
- }
115
-
116
- // TODO: Support multiple matches
117
- samlConfig = samlConfigs[0];
118
- } else {
119
- samlConfig = await this.configStore.get(client_id);
120
- }
121
- } else {
122
- throw new JacksonError(
123
- 'You need to specify client_id or tenant & product',
124
- 403
125
- );
126
- }
127
-
128
- if (!samlConfig) {
129
- throw new JacksonError('SAML configuration not found.', 403);
130
- }
131
-
132
- if (!allowed.redirect(redirect_uri, samlConfig.redirectUrl)) {
133
- throw new JacksonError('Redirect URL is not allowed.', 403);
134
- }
135
-
136
- const samlReq = saml.request({
137
- entityID: this.opts.samlAudience,
138
- callbackUrl: this.opts.externalUrl + this.opts.samlPath,
139
- signingKey: samlConfig.certs.privateKey,
140
- });
141
-
142
- const sessionId = crypto.randomBytes(16).toString('hex');
143
-
144
- await this.sessionStore.put(sessionId, {
145
- id: samlReq.id,
146
- redirect_uri,
147
- response_type,
148
- state,
149
- code_challenge,
150
- code_challenge_method,
151
- });
152
-
153
- const redirectUrl = redirect.success(
154
- samlConfig.idpMetadata.sso.redirectUrl,
155
- {
156
- RelayState: relayStatePrefix + sessionId,
157
- SAMLRequest: Buffer.from(samlReq.request).toString('base64'),
158
- }
159
- );
160
-
161
- return { redirect_url: redirectUrl };
162
- }
163
-
164
- public async samlResponse(
165
- body: SAMLResponsePayload
166
- ): Promise<{ redirect_url: string }> {
167
- const { SAMLResponse } = body; // RelayState will contain the sessionId from earlier quasi-oauth flow
168
-
169
- let RelayState = body.RelayState || '';
170
-
171
- if (!this.opts.idpEnabled && !RelayState.startsWith(relayStatePrefix)) {
172
- // IDP is disabled so block the request
173
-
174
- throw new JacksonError(
175
- 'IdP (Identity Provider) flow has been disabled. Please head to your Service Provider to login.',
176
- 403
177
- );
178
- }
179
-
180
- if (!RelayState.startsWith(relayStatePrefix)) {
181
- RelayState = '';
182
- }
183
-
184
- RelayState = RelayState.replace(relayStatePrefix, '');
185
-
186
- const rawResponse = Buffer.from(SAMLResponse, 'base64').toString();
187
-
188
- const parsedResp = await saml.parseAsync(rawResponse);
189
-
190
- const samlConfigs = await this.configStore.getByIndex({
191
- name: IndexNames.EntityID,
192
- value: parsedResp?.issuer,
193
- });
194
-
195
- if (!samlConfigs || samlConfigs.length === 0) {
196
- throw new JacksonError('SAML configuration not found.', 403);
197
- }
198
-
199
- // TODO: Support multiple matches
200
- const samlConfig = samlConfigs[0];
201
-
202
- let session;
203
-
204
- if (RelayState !== '') {
205
- session = await this.sessionStore.get(RelayState);
206
- if (!session) {
207
- throw new JacksonError(
208
- 'Unable to validate state from the origin request.',
209
- 403
210
- );
211
- }
212
- }
213
-
214
- const validateOpts: Record<string, string> = {
215
- thumbprint: samlConfig.idpMetadata.thumbprint,
216
- audience: this.opts.samlAudience,
217
- };
218
-
219
- if (session && session.id) {
220
- validateOpts.inResponseTo = session.id;
221
- }
222
-
223
- const profile = await saml.validateAsync(rawResponse, validateOpts);
224
-
225
- // store details against a code
226
- const code = crypto.randomBytes(20).toString('hex');
227
-
228
- const codeVal: Record<string, unknown> = {
229
- profile,
230
- clientID: samlConfig.clientID,
231
- clientSecret: samlConfig.clientSecret,
232
- };
233
-
234
- if (session) {
235
- codeVal.session = session;
236
- }
237
-
238
- await this.codeStore.put(code, codeVal);
239
-
240
- if (
241
- session &&
242
- session.redirect_uri &&
243
- !allowed.redirect(session.redirect_uri, samlConfig.redirectUrl)
244
- ) {
245
- throw new JacksonError('Redirect URL is not allowed.', 403);
246
- }
247
-
248
- const params: Record<string, string> = {
249
- code,
250
- };
251
-
252
- if (session && session.state) {
253
- params.state = session.state;
254
- }
255
-
256
- const redirectUrl = redirect.success(
257
- (session && session.redirect_uri) || samlConfig.defaultRedirectUrl,
258
- params
259
- );
260
-
261
- return { redirect_url: redirectUrl };
262
- }
263
-
264
- public async token(body: OAuthTokenReq): Promise<OAuthTokenRes> {
265
- const {
266
- client_id,
267
- client_secret,
268
- code_verifier,
269
- code,
270
- grant_type = 'authorization_code',
271
- } = body;
272
-
273
- if (grant_type !== 'authorization_code') {
274
- throw new JacksonError('Unsupported grant_type', 400);
275
- }
276
-
277
- if (!code) {
278
- throw new JacksonError('Please specify code', 400);
279
- }
280
-
281
- const codeVal = await this.codeStore.get(code);
282
- if (!codeVal || !codeVal.profile) {
283
- throw new JacksonError('Invalid code', 403);
284
- }
285
-
286
- if (client_id && client_secret) {
287
- // check if we have an encoded client_id
288
- if (client_id !== 'dummy' && client_secret !== 'dummy') {
289
- const sp = getEncodedClientId(client_id);
290
- if (!sp) {
291
- // OAuth flow
292
- if (
293
- client_id !== codeVal.clientID ||
294
- client_secret !== codeVal.clientSecret
295
- ) {
296
- throw new JacksonError('Invalid client_id or client_secret', 401);
297
- }
298
- }
299
- }
300
- } else if (code_verifier) {
301
- // PKCE flow
302
- let cv = code_verifier;
303
- if (codeVal.session.code_challenge_method.toLowerCase() === 's256') {
304
- cv = codeVerifier.encode(code_verifier);
305
- }
306
-
307
- if (codeVal.session.code_challenge !== cv) {
308
- throw new JacksonError('Invalid code_verifier', 401);
309
- }
310
- } else if (codeVal && codeVal.session) {
311
- throw new JacksonError(
312
- 'Please specify client_secret or code_verifier',
313
- 401
314
- );
315
- }
316
-
317
- // store details against a token
318
- const token = crypto.randomBytes(20).toString('hex');
319
-
320
- await this.tokenStore.put(token, codeVal.profile);
321
-
322
- return {
323
- access_token: token,
324
- token_type: 'bearer',
325
- expires_in: this.opts.db.ttl,
326
- };
327
- }
328
-
329
- public async userInfo(token: string): Promise<Profile> {
330
- const { claims } = await this.tokenStore.get(token);
331
-
332
- return claims;
333
- }
334
- }
@@ -1,17 +0,0 @@
1
- import { Request } from 'express';
2
-
3
- export const extractAuthToken = (req: Request): string | null => {
4
- const authHeader = req.get('authorization');
5
- const parts = (authHeader || '').split(' ');
6
-
7
- if (parts.length > 1) {
8
- return parts[1];
9
- }
10
-
11
- return null;
12
- };
13
-
14
- export enum IndexNames {
15
- EntityID = 'entityID',
16
- TenantProduct = 'tenantProduct',
17
- }
package/src/db/db.ts DELETED
@@ -1,100 +0,0 @@
1
- import {
2
- DatabaseDriver,
3
- DatabaseOption,
4
- Encrypted,
5
- EncryptionKey,
6
- Index,
7
- Storable,
8
- } from 'saml-jackson';
9
- import * as encrypter from './encrypter';
10
- import mem from './mem';
11
- import mongo from './mongo';
12
- import redis from './redis';
13
- import sql from './sql/sql';
14
- import store from './store';
15
-
16
- const decrypt = (res: Encrypted, encryptionKey: EncryptionKey): unknown => {
17
- if (res.iv && res.tag) {
18
- return JSON.parse(
19
- encrypter.decrypt(res.value, res.iv, res.tag, encryptionKey)
20
- );
21
- }
22
-
23
- return JSON.parse(res.value);
24
- };
25
-
26
- class DB implements DatabaseDriver {
27
- private db: DatabaseDriver;
28
- private encryptionKey: EncryptionKey;
29
-
30
- constructor(db: DatabaseDriver, encryptionKey: EncryptionKey) {
31
- this.db = db;
32
- this.encryptionKey = encryptionKey;
33
- }
34
-
35
- async get(namespace: string, key: string): Promise<unknown> {
36
- const res = await this.db.get(namespace, key);
37
-
38
- if (!res) {
39
- return null;
40
- }
41
-
42
- return decrypt(res, this.encryptionKey);
43
- }
44
-
45
- async getByIndex(namespace: string, idx: Index): Promise<unknown[]> {
46
- const res = await this.db.getByIndex(namespace, idx);
47
- const encryptionKey = this.encryptionKey;
48
- return res.map((r) => {
49
- return decrypt(r, encryptionKey);
50
- });
51
- }
52
-
53
- // ttl is in seconds
54
- async put(
55
- namespace: string,
56
- key: string,
57
- val: unknown,
58
- ttl = 0,
59
- ...indexes: Index[]
60
- ): Promise<unknown> {
61
- if (ttl > 0 && indexes && indexes.length > 0) {
62
- throw new Error('secondary indexes not allow on a store with ttl');
63
- }
64
-
65
- const dbVal = this.encryptionKey
66
- ? encrypter.encrypt(JSON.stringify(val), this.encryptionKey)
67
- : { value: JSON.stringify(val) };
68
-
69
- return await this.db.put(namespace, key, dbVal, ttl, ...indexes);
70
- }
71
-
72
- async delete(namespace: string, key: string): Promise<unknown> {
73
- return await this.db.delete(namespace, key);
74
- }
75
-
76
- store(namespace: string, ttl = 0): Storable {
77
- return store.new(namespace, this, ttl);
78
- }
79
- }
80
-
81
- export = {
82
- new: async (options: DatabaseOption) => {
83
- const encryptionKey = options.encryptionKey
84
- ? Buffer.from(options.encryptionKey, 'latin1')
85
- : null;
86
-
87
- switch (options.engine) {
88
- case 'redis':
89
- return new DB(await redis.new(options), encryptionKey);
90
- case 'sql':
91
- return new DB(await sql.new(options), encryptionKey);
92
- case 'mongo':
93
- return new DB(await mongo.new(options), encryptionKey);
94
- case 'mem':
95
- return new DB(await mem.new(options), encryptionKey);
96
- default:
97
- throw new Error('unsupported db engine: ' + options.engine);
98
- }
99
- },
100
- };
@@ -1,38 +0,0 @@
1
- import crypto from 'crypto';
2
- import { Encrypted, EncryptionKey } from 'saml-jackson';
3
-
4
- const ALGO = 'aes-256-gcm';
5
- const BLOCK_SIZE = 16; // 128 bit
6
-
7
- export const encrypt = (text: string, key: EncryptionKey): Encrypted => {
8
- const iv = crypto.randomBytes(BLOCK_SIZE);
9
- const cipher = crypto.createCipheriv(ALGO, key, iv);
10
-
11
- let ciphertext = cipher.update(text, 'utf8', 'base64');
12
- ciphertext += cipher.final('base64');
13
-
14
- return {
15
- iv: iv.toString('base64'),
16
- tag: cipher.getAuthTag().toString('base64'),
17
- value: ciphertext,
18
- };
19
- };
20
-
21
- export const decrypt = (
22
- ciphertext: string,
23
- iv: string,
24
- tag: string,
25
- key: EncryptionKey
26
- ): string => {
27
- const decipher = crypto.createDecipheriv(
28
- ALGO,
29
- key,
30
- Buffer.from(iv, 'base64')
31
- );
32
- decipher.setAuthTag(Buffer.from(tag, 'base64'));
33
-
34
- let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
35
- cleartext += decipher.final('utf8');
36
-
37
- return cleartext;
38
- };
package/src/db/mem.ts DELETED
@@ -1,128 +0,0 @@
1
- // This is an in-memory implementation to be used with testing and prototyping only
2
-
3
- import { DatabaseDriver, DatabaseOption, Index, Encrypted } from 'saml-jackson';
4
- import * as dbutils from './utils';
5
-
6
- class Mem implements DatabaseDriver {
7
- private options: DatabaseOption;
8
- private store: any;
9
- private indexes: any;
10
- private cleanup: any;
11
- private ttlStore: any;
12
- private ttlCleanup: any;
13
- private timerId: any;
14
-
15
- constructor(options: DatabaseOption) {
16
- this.options = options;
17
- }
18
-
19
- async init(): Promise<Mem> {
20
- this.store = {}; // map of key, value
21
- this.indexes = {}; // map of key, Set
22
- this.cleanup = {}; // map of indexes for cleanup when store key is deleted
23
- this.ttlStore = {}; // map of key to ttl
24
-
25
- if (this.options.ttl) {
26
- this.ttlCleanup = async () => {
27
- const now = Date.now();
28
- for (const k in this.ttlStore) {
29
- if (this.ttlStore[k].expiresAt < now) {
30
- await this.delete(this.ttlStore[k].namespace, this.ttlStore[k].key);
31
- }
32
- }
33
-
34
- if (this.options.ttl) {
35
- this.timerId = setTimeout(this.ttlCleanup, this.options.ttl * 1000);
36
- }
37
- };
38
-
39
- this.timerId = setTimeout(this.ttlCleanup, this.options.ttl * 1000);
40
- }
41
-
42
- return this;
43
- }
44
-
45
- async get(namespace: string, key: string): Promise<any> {
46
- const res = this.store[dbutils.key(namespace, key)];
47
- if (res) {
48
- return res;
49
- }
50
-
51
- return null;
52
- }
53
-
54
- async getByIndex(namespace: string, idx: Index): Promise<any> {
55
- const dbKeys = await this.indexes[dbutils.keyForIndex(namespace, idx)];
56
-
57
- const ret: string[] = [];
58
- for (const dbKey of dbKeys || []) {
59
- ret.push(await this.get(namespace, dbKey));
60
- }
61
-
62
- return ret;
63
- }
64
-
65
- async put(
66
- namespace: string,
67
- key: string,
68
- val: Encrypted,
69
- ttl = 0,
70
- ...indexes: any[]
71
- ): Promise<any> {
72
- const k = dbutils.key(namespace, key);
73
-
74
- this.store[k] = val;
75
-
76
- if (ttl) {
77
- this.ttlStore[k] = {
78
- namespace,
79
- key,
80
- expiresAt: Date.now() + ttl * 1000,
81
- };
82
- }
83
-
84
- // no ttl support for secondary indexes
85
- for (const idx of indexes || []) {
86
- const idxKey = dbutils.keyForIndex(namespace, idx);
87
- let set = this.indexes[idxKey];
88
- if (!set) {
89
- set = new Set();
90
- this.indexes[idxKey] = set;
91
- }
92
-
93
- set.add(key);
94
-
95
- const cleanupKey = dbutils.keyFromParts(dbutils.indexPrefix, k);
96
- let cleanup = this.cleanup[cleanupKey];
97
- if (!cleanup) {
98
- cleanup = new Set();
99
- this.cleanup[cleanupKey] = cleanup;
100
- }
101
-
102
- cleanup.add(idxKey);
103
- }
104
- }
105
-
106
- async delete(namespace: string, key: string): Promise<any> {
107
- const k = dbutils.key(namespace, key);
108
-
109
- delete this.store[k];
110
-
111
- const idxKey = dbutils.keyFromParts(dbutils.indexPrefix, k);
112
- // delete secondary indexes and then the mapping of the seconary indexes
113
- const dbKeys = this.cleanup[idxKey];
114
-
115
- for (const dbKey of dbKeys || []) {
116
- this.indexes[dbKey] && this.indexes[dbKey].delete(key);
117
- }
118
-
119
- delete this.cleanup[idxKey];
120
- delete this.ttlStore[k];
121
- }
122
- }
123
-
124
- export default {
125
- new: async (options: DatabaseOption) => {
126
- return await new Mem(options).init();
127
- },
128
- };