@boxyhq/saml-jackson 0.2.3-beta.228 → 0.2.3-beta.233

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 (49) hide show
  1. package/package.json +8 -2
  2. package/ nodemon.json +0 -12
  3. package/.dockerignore +0 -2
  4. package/.eslintrc.js +0 -18
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  6. package/.github/ISSUE_TEMPLATE/config.yml +0 -5
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -43
  8. package/.github/pull_request_template.md +0 -31
  9. package/.github/workflows/codesee-arch-diagram.yml +0 -81
  10. package/.github/workflows/main.yml +0 -123
  11. package/_dev/docker-compose.yml +0 -37
  12. package/map.js +0 -1
  13. package/prettier.config.js +0 -4
  14. package/src/controller/api.ts +0 -225
  15. package/src/controller/error.ts +0 -13
  16. package/src/controller/oauth/allowed.ts +0 -22
  17. package/src/controller/oauth/code-verifier.ts +0 -11
  18. package/src/controller/oauth/redirect.ts +0 -12
  19. package/src/controller/oauth.ts +0 -337
  20. package/src/controller/utils.ts +0 -17
  21. package/src/db/db.ts +0 -100
  22. package/src/db/encrypter.ts +0 -38
  23. package/src/db/mem.ts +0 -128
  24. package/src/db/mongo.ts +0 -110
  25. package/src/db/redis.ts +0 -103
  26. package/src/db/sql/entity/JacksonIndex.ts +0 -44
  27. package/src/db/sql/entity/JacksonStore.ts +0 -43
  28. package/src/db/sql/entity/JacksonTTL.ts +0 -17
  29. package/src/db/sql/model/JacksonIndex.ts +0 -3
  30. package/src/db/sql/model/JacksonStore.ts +0 -8
  31. package/src/db/sql/sql.ts +0 -181
  32. package/src/db/store.ts +0 -49
  33. package/src/db/utils.ts +0 -26
  34. package/src/env.ts +0 -42
  35. package/src/index.ts +0 -79
  36. package/src/jackson.ts +0 -171
  37. package/src/read-config.ts +0 -29
  38. package/src/saml/claims.ts +0 -41
  39. package/src/saml/saml.ts +0 -234
  40. package/src/saml/x509.ts +0 -51
  41. package/src/test/api.test.ts +0 -270
  42. package/src/test/data/metadata/boxyhq.js +0 -6
  43. package/src/test/data/metadata/boxyhq.xml +0 -30
  44. package/src/test/data/saml_response +0 -1
  45. package/src/test/db.test.ts +0 -313
  46. package/src/test/oauth.test.ts +0 -362
  47. package/src/typings.ts +0 -167
  48. package/tsconfig.build.json +0 -6
  49. package/tsconfig.json +0 -26
@@ -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,337 +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 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
- // @ts-ignore
138
- entityID: this.opts.samlAudience,
139
- callbackUrl: this.opts.externalUrl + this.opts.samlPath,
140
- signingKey: samlConfig.certs.privateKey,
141
- });
142
-
143
- const sessionId = crypto.randomBytes(16).toString('hex');
144
-
145
- await this.sessionStore.put(sessionId, {
146
- id: samlReq.id,
147
- redirect_uri,
148
- response_type,
149
- state,
150
- code_challenge,
151
- code_challenge_method,
152
- });
153
-
154
- const redirectUrl = redirect.success(
155
- samlConfig.idpMetadata.sso.redirectUrl,
156
- {
157
- RelayState: relayStatePrefix + sessionId,
158
- SAMLRequest: Buffer.from(samlReq.request).toString('base64'),
159
- }
160
- );
161
-
162
- return { redirect_url: redirectUrl };
163
- }
164
-
165
- public async samlResponse(
166
- body: SAMLResponsePayload
167
- ): Promise<{ redirect_url: string }> {
168
- const { SAMLResponse } = body; // RelayState will contain the sessionId from earlier quasi-oauth flow
169
-
170
- let RelayState = body.RelayState || '';
171
-
172
- if (!this.opts.idpEnabled && !RelayState.startsWith(relayStatePrefix)) {
173
- // IDP is disabled so block the request
174
-
175
- throw new JacksonError(
176
- 'IdP (Identity Provider) flow has been disabled. Please head to your Service Provider to login.',
177
- 403
178
- );
179
- }
180
-
181
- if (!RelayState.startsWith(relayStatePrefix)) {
182
- RelayState = '';
183
- }
184
-
185
- RelayState = RelayState.replace(relayStatePrefix, '');
186
-
187
- const rawResponse = Buffer.from(SAMLResponse, 'base64').toString();
188
-
189
- const parsedResp = await saml.parseAsync(rawResponse);
190
-
191
- const samlConfigs = await this.configStore.getByIndex({
192
- name: IndexNames.EntityID,
193
-
194
- // @ts-ignore
195
- value: parsedResp?.issuer,
196
- });
197
-
198
- if (!samlConfigs || samlConfigs.length === 0) {
199
- throw new JacksonError('SAML configuration not found.', 403);
200
- }
201
-
202
- // TODO: Support multiple matches
203
- const samlConfig = samlConfigs[0];
204
-
205
- let session;
206
-
207
- if (RelayState !== '') {
208
- session = await this.sessionStore.get(RelayState);
209
- if (!session) {
210
- throw new JacksonError(
211
- 'Unable to validate state from the origin request.',
212
- 403
213
- );
214
- }
215
- }
216
-
217
- let validateOpts: any = {
218
- thumbprint: samlConfig.idpMetadata.thumbprint,
219
- audience: this.opts.samlAudience,
220
- };
221
-
222
- if (session && session.id) {
223
- validateOpts.inResponseTo = session.id;
224
- }
225
-
226
- const profile = await saml.validateAsync(rawResponse, validateOpts);
227
-
228
- // store details against a code
229
- const code = crypto.randomBytes(20).toString('hex');
230
-
231
- let codeVal: any = {
232
- profile,
233
- clientID: samlConfig.clientID,
234
- clientSecret: samlConfig.clientSecret,
235
- };
236
-
237
- if (session) {
238
- codeVal.session = session;
239
- }
240
-
241
- await this.codeStore.put(code, codeVal);
242
-
243
- if (
244
- session &&
245
- session.redirect_uri &&
246
- !allowed.redirect(session.redirect_uri, samlConfig.redirectUrl)
247
- ) {
248
- throw new JacksonError('Redirect URL is not allowed.', 403);
249
- }
250
-
251
- let params: any = {
252
- code,
253
- };
254
-
255
- if (session && session.state) {
256
- params.state = session.state;
257
- }
258
-
259
- const redirectUrl = redirect.success(
260
- (session && session.redirect_uri) || samlConfig.defaultRedirectUrl,
261
- params
262
- );
263
-
264
- return { redirect_url: redirectUrl };
265
- }
266
-
267
- public async token(body: OAuthTokenReq): Promise<OAuthTokenRes> {
268
- const {
269
- client_id,
270
- client_secret,
271
- code_verifier,
272
- code,
273
- grant_type = 'authorization_code',
274
- } = body;
275
-
276
- if (grant_type !== 'authorization_code') {
277
- throw new JacksonError('Unsupported grant_type', 400);
278
- }
279
-
280
- if (!code) {
281
- throw new JacksonError('Please specify code', 400);
282
- }
283
-
284
- const codeVal = await this.codeStore.get(code);
285
- if (!codeVal || !codeVal.profile) {
286
- throw new JacksonError('Invalid code', 403);
287
- }
288
-
289
- if (client_id && client_secret) {
290
- // check if we have an encoded client_id
291
- if (client_id !== 'dummy' && client_secret !== 'dummy') {
292
- const sp = getEncodedClientId(client_id);
293
- if (!sp) {
294
- // OAuth flow
295
- if (
296
- client_id !== codeVal.clientID ||
297
- client_secret !== codeVal.clientSecret
298
- ) {
299
- throw new JacksonError('Invalid client_id or client_secret', 401);
300
- }
301
- }
302
- }
303
- } else if (code_verifier) {
304
- // PKCE flow
305
- let cv = code_verifier;
306
- if (codeVal.session.code_challenge_method.toLowerCase() === 's256') {
307
- cv = codeVerifier.encode(code_verifier);
308
- }
309
-
310
- if (codeVal.session.code_challenge !== cv) {
311
- throw new JacksonError('Invalid code_verifier', 401);
312
- }
313
- } else if (codeVal && codeVal.session) {
314
- throw new JacksonError(
315
- 'Please specify client_secret or code_verifier',
316
- 401
317
- );
318
- }
319
-
320
- // store details against a token
321
- const token = crypto.randomBytes(20).toString('hex');
322
-
323
- await this.tokenStore.put(token, codeVal.profile);
324
-
325
- return {
326
- access_token: token,
327
- token_type: 'bearer',
328
- expires_in: this.opts.db.ttl,
329
- };
330
- }
331
-
332
- public async userInfo(token: string): Promise<Profile> {
333
- const { claims } = await this.tokenStore.get(token);
334
-
335
- return claims;
336
- }
337
- }
@@ -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): any => {
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: any;
28
- private encryptionKey: EncryptionKey;
29
-
30
- constructor(db: any, encryptionKey: EncryptionKey) {
31
- this.db = db;
32
- this.encryptionKey = encryptionKey;
33
- }
34
-
35
- async get(namespace: string, key: string): Promise<any> {
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<any> {
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: any,
58
- ttl: number = 0,
59
- ...indexes: any[]
60
- ): Promise<any> {
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<any> {
73
- return await this.db.delete(namespace, key);
74
- }
75
-
76
- store(namespace: string, ttl: number = 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
- let 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: number = 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
- };