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

Sign up to get free protection for your applications and to get access to all the features.
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,362 +0,0 @@
1
- import crypto from 'crypto';
2
- import { promises as fs } from 'fs';
3
- import path from 'path';
4
- import {
5
- IOAuthController,
6
- ISAMLConfig,
7
- JacksonOption,
8
- OAuthReqBody,
9
- OAuthTokenReq,
10
- SAMLResponsePayload,
11
- } from 'saml-jackson';
12
- import sinon from 'sinon';
13
- import tap from 'tap';
14
- import { JacksonError } from '../controller/error';
15
- import readConfig from '../read-config';
16
- import saml from '../saml/saml';
17
-
18
- let apiController: ISAMLConfig;
19
- let oauthController: IOAuthController;
20
-
21
- const code = '1234567890';
22
- const token = '24c1550190dd6a5a9bd6fe2a8ff69d593121c7b9';
23
-
24
- const metadataPath = path.join(__dirname, '/data/metadata');
25
-
26
- const options = <JacksonOption>{
27
- externalUrl: 'https://my-cool-app.com',
28
- samlAudience: 'https://saml.boxyhq.com',
29
- samlPath: '/sso/oauth/saml',
30
- db: {
31
- engine: 'mem',
32
- },
33
- };
34
-
35
- const samlConfig = {
36
- tenant: 'boxyhq.com',
37
- product: 'crm',
38
- redirectUrl: '["http://localhost:3000/*"]',
39
- defaultRedirectUrl: 'http://localhost:3000/login/saml',
40
- rawMetadata: null,
41
- };
42
-
43
- const addMetadata = async (metadataPath) => {
44
- const configs = await readConfig(metadataPath);
45
-
46
- for (const config of configs) {
47
- await apiController.config(config);
48
- }
49
- };
50
-
51
- tap.before(async () => {
52
- const controller = await (await import('../index')).default(options);
53
-
54
- apiController = controller.apiController;
55
- oauthController = controller.oauthController;
56
-
57
- await addMetadata(metadataPath);
58
- });
59
-
60
- tap.teardown(async () => {
61
- process.exit(0);
62
- });
63
-
64
- tap.test('authorize()', async (t) => {
65
- t.test('Should throw an error if `redirect_uri` null', async (t) => {
66
- const body: Partial<OAuthReqBody> = {
67
- redirect_uri: undefined,
68
- state: 'state',
69
- };
70
-
71
- try {
72
- await oauthController.authorize(<OAuthReqBody>body);
73
- t.fail('Expecting JacksonError.');
74
- } catch (err) {
75
- const { message, statusCode } = err as JacksonError;
76
- t.equal(
77
- message,
78
- 'Please specify a redirect URL.',
79
- 'got expected error message'
80
- );
81
- t.equal(statusCode, 400, 'got expected status code');
82
- }
83
-
84
- t.end();
85
- });
86
-
87
- t.test('Should throw an error if `state` null', async (t) => {
88
- const body: Partial<OAuthReqBody> = {
89
- redirect_uri: 'https://example.com/',
90
- state: undefined,
91
- };
92
-
93
- try {
94
- await oauthController.authorize(<OAuthReqBody>body);
95
-
96
- t.fail('Expecting JacksonError.');
97
- } catch (err) {
98
- const { message, statusCode } = err as JacksonError;
99
- t.equal(
100
- message,
101
- 'Please specify a state to safeguard against XSRF attacks.',
102
- 'got expected error message'
103
- );
104
- t.equal(statusCode, 400, 'got expected status code');
105
- }
106
-
107
- t.end();
108
- });
109
-
110
- t.test('Should throw an error if `client_id` is invalid', async (t) => {
111
- const body = {
112
- redirect_uri: 'https://example.com/',
113
- state: 'state-123',
114
- client_id: '27fa9a11875ec3a0',
115
- };
116
-
117
- try {
118
- await oauthController.authorize(<OAuthReqBody>body);
119
-
120
- t.fail('Expecting JacksonError.');
121
- } catch (err) {
122
- const { message, statusCode } = err as JacksonError;
123
- t.equal(
124
- message,
125
- 'SAML configuration not found.',
126
- 'got expected error message'
127
- );
128
- t.equal(statusCode, 403, 'got expected status code');
129
- }
130
-
131
- t.end();
132
- });
133
-
134
- t.test(
135
- 'Should throw an error if `redirect_uri` is not allowed',
136
- async (t) => {
137
- const body = {
138
- redirect_uri: 'https://example.com/',
139
- state: 'state-123',
140
- client_id: `tenant=${samlConfig.tenant}&product=${samlConfig.product}`,
141
- };
142
-
143
- try {
144
- await oauthController.authorize(<OAuthReqBody>body);
145
-
146
- t.fail('Expecting JacksonError.');
147
- } catch (err) {
148
- const { message, statusCode } = err as JacksonError;
149
- t.equal(
150
- message,
151
- 'Redirect URL is not allowed.',
152
- 'got expected error message'
153
- );
154
- t.equal(statusCode, 403, 'got expected status code');
155
- }
156
-
157
- t.end();
158
- }
159
- );
160
-
161
- t.test('Should return the Idp SSO URL', async (t) => {
162
- const body = {
163
- redirect_uri: samlConfig.defaultRedirectUrl,
164
- state: 'state-123',
165
- client_id: `tenant=${samlConfig.tenant}&product=${samlConfig.product}`,
166
- };
167
-
168
- const response = await oauthController.authorize(<OAuthReqBody>body);
169
- const params = new URLSearchParams(new URL(response.redirect_url).search);
170
-
171
- t.ok('redirect_url' in response, 'got the Idp authorize URL');
172
- t.ok(params.has('RelayState'), 'RelayState present in the query string');
173
- t.ok(params.has('SAMLRequest'), 'SAMLRequest present in the query string');
174
-
175
- t.end();
176
- });
177
-
178
- t.end();
179
- });
180
-
181
- tap.test('samlResponse()', async (t) => {
182
- const authBody = {
183
- redirect_uri: samlConfig.defaultRedirectUrl,
184
- state: 'state-123',
185
- client_id: `tenant=${samlConfig.tenant}&product=${samlConfig.product}`,
186
- };
187
-
188
- const { redirect_url } = await oauthController.authorize(
189
- <OAuthReqBody>authBody
190
- );
191
-
192
- const relayState = new URLSearchParams(new URL(redirect_url).search).get(
193
- 'RelayState'
194
- );
195
-
196
- const rawResponse = await fs.readFile(
197
- path.join(__dirname, '/data/saml_response'),
198
- 'utf8'
199
- );
200
-
201
- t.test('Should throw an error if `RelayState` is missing', async (t) => {
202
- const responseBody: Partial<SAMLResponsePayload> = {
203
- SAMLResponse: rawResponse,
204
- };
205
-
206
- try {
207
- await oauthController.samlResponse(<SAMLResponsePayload>responseBody);
208
-
209
- t.fail('Expecting JacksonError.');
210
- } catch (err) {
211
- const { message, statusCode } = err as JacksonError;
212
- t.equal(
213
- message,
214
- 'IdP (Identity Provider) flow has been disabled. Please head to your Service Provider to login.',
215
- 'got expected error message'
216
- );
217
-
218
- t.equal(statusCode, 403, 'got expected status code');
219
- }
220
-
221
- t.end();
222
- });
223
-
224
- t.test(
225
- 'Should return a URL with code and state as query params',
226
- async (t) => {
227
- const responseBody = {
228
- SAMLResponse: rawResponse,
229
- RelayState: relayState,
230
- };
231
-
232
- const stubValidateAsync = sinon
233
- .stub(saml, 'validateAsync')
234
- .resolves({ audience: '', claims: {}, issuer: '', sessionIndex: '' });
235
-
236
- //@ts-ignore
237
- const stubRandomBytes = sinon.stub(crypto, 'randomBytes').returns(code);
238
-
239
- const response = await oauthController.samlResponse(
240
- <SAMLResponsePayload>responseBody
241
- );
242
-
243
- const params = new URLSearchParams(new URL(response.redirect_url).search);
244
-
245
- t.ok(stubValidateAsync.calledOnce, 'validateAsync called once');
246
- t.ok(stubRandomBytes.calledOnce, 'randomBytes called once');
247
- t.ok('redirect_url' in response, 'response contains redirect_url');
248
- t.ok(params.has('code'), 'query string includes code');
249
- t.ok(params.has('state'), 'query string includes state');
250
- t.match(params.get('state'), authBody.state, 'state value is valid');
251
-
252
- stubRandomBytes.restore();
253
- stubValidateAsync.restore();
254
-
255
- t.end();
256
- }
257
- );
258
-
259
- t.end();
260
- });
261
-
262
- tap.test('token()', (t) => {
263
- t.test(
264
- 'Should throw an error if `grant_type` is not `authorization_code`',
265
- async (t) => {
266
- const body = {
267
- grant_type: 'authorization_code_1',
268
- };
269
-
270
- try {
271
- await oauthController.token(<OAuthTokenReq>body);
272
-
273
- t.fail('Expecting JacksonError.');
274
- } catch (err) {
275
- const { message, statusCode } = err as JacksonError;
276
- t.equal(
277
- message,
278
- 'Unsupported grant_type',
279
- 'got expected error message'
280
- );
281
- t.equal(statusCode, 400, 'got expected status code');
282
- }
283
-
284
- t.end();
285
- }
286
- );
287
-
288
- t.test('Should throw an error if `code` is missing', async (t) => {
289
- const body = {
290
- grant_type: 'authorization_code',
291
- };
292
-
293
- try {
294
- await oauthController.token(<OAuthTokenReq>body);
295
-
296
- t.fail('Expecting JacksonError.');
297
- } catch (err) {
298
- const { message, statusCode } = err as JacksonError;
299
- t.equal(message, 'Please specify code', 'got expected error message');
300
- t.equal(statusCode, 400, 'got expected status code');
301
- }
302
-
303
- t.end();
304
- });
305
-
306
- t.test('Should throw an error if `code` is invalid', async (t) => {
307
- const body: Partial<OAuthTokenReq> = {
308
- grant_type: 'authorization_code',
309
- client_id: `tenant=${samlConfig.tenant}&product=${samlConfig.product}`,
310
- client_secret: 'some-secret',
311
- code: 'invalid-code',
312
- };
313
-
314
- try {
315
- await oauthController.token(<OAuthTokenReq>body);
316
-
317
- t.fail('Expecting JacksonError.');
318
- } catch (err) {
319
- const { message, statusCode } = err as JacksonError;
320
- t.equal(message, 'Invalid code', 'got expected error message');
321
- t.equal(statusCode, 403, 'got expected status code');
322
- }
323
-
324
- t.end();
325
- });
326
-
327
- t.test('Should return the `access_token` for a valid request', async (t) => {
328
- const body: Partial<OAuthTokenReq> = {
329
- grant_type: 'authorization_code',
330
- client_id: `tenant=${samlConfig.tenant}&product=${samlConfig.product}`,
331
- client_secret: 'some-secret',
332
- code: code,
333
- };
334
-
335
- const stubRandomBytes = sinon
336
- .stub(crypto, 'randomBytes')
337
- .onFirstCall()
338
- //@ts-ignore
339
- .returns(token);
340
-
341
- const response = await oauthController.token(<OAuthTokenReq>body);
342
-
343
- t.ok(stubRandomBytes.calledOnce, 'randomBytes called once');
344
- t.ok('access_token' in response, 'includes access_token');
345
- t.ok('token_type' in response, 'includes token_type');
346
- t.ok('expires_in' in response, 'includes expires_in');
347
- t.match(response.access_token, token);
348
- t.match(response.token_type, 'bearer');
349
- t.match(response.expires_in, 300);
350
-
351
- stubRandomBytes.restore();
352
-
353
- t.end();
354
- });
355
-
356
- // TODO
357
- t.test('Handle invalid client_id', async (t) => {
358
- t.end();
359
- });
360
-
361
- t.end();
362
- });
package/src/typings.ts DELETED
@@ -1,167 +0,0 @@
1
- declare module 'saml-jackson' {
2
- export type IdPConfig = {
3
- defaultRedirectUrl: string;
4
- redirectUrl: string;
5
- tenant: string;
6
- product: string;
7
- rawMetadata: string;
8
- };
9
-
10
- export interface OAuth {
11
- client_id: string;
12
- client_secret: string;
13
- provider: string;
14
- }
15
-
16
- export interface ISAMLConfig {
17
- // Ensure backward compatibility
18
- config(body: IdPConfig): Promise<OAuth>;
19
-
20
- getConfig(body: {
21
- clientID: string;
22
- tenant: string;
23
- product: string;
24
- }): Promise<Partial<OAuth>>;
25
-
26
- deleteConfig(body: {
27
- clientID: string;
28
- clientSecret: string;
29
- tenant: string;
30
- product: string;
31
- }): Promise<void>;
32
-
33
- // New methods
34
- create(body: IdPConfig): Promise<OAuth>;
35
-
36
- get(body: {
37
- clientID: string;
38
- tenant: string;
39
- product: string;
40
- }): Promise<Partial<OAuth>>;
41
-
42
- delete(body: {
43
- clientID: string;
44
- clientSecret: string;
45
- tenant: string;
46
- product: string;
47
- }): Promise<void>;
48
- }
49
-
50
- export interface IOAuthController {
51
- authorize(body: OAuthReqBody): Promise<{ redirect_url: string }>;
52
- samlResponse(body: SAMLResponsePayload): Promise<{ redirect_url: string }>;
53
- token(body: OAuthTokenReq): Promise<OAuthTokenRes>;
54
- userInfo(token: string): Promise<Profile>;
55
- }
56
-
57
- export interface OAuthReqBody {
58
- response_type: 'code';
59
- client_id: string;
60
- redirect_uri: string;
61
- state: string;
62
- tenant: string;
63
- product: string;
64
- code_challenge: string;
65
- code_challenge_method: 'plain' | 'S256' | '';
66
- provider: 'saml';
67
- }
68
-
69
- export interface SAMLResponsePayload {
70
- SAMLResponse: string;
71
- RelayState: string;
72
- }
73
-
74
- export interface OAuthTokenReq {
75
- client_id: string;
76
- client_secret: string;
77
- code_verifier: string;
78
- code: string;
79
- grant_type: 'authorization_code';
80
- }
81
-
82
- export interface OAuthTokenRes {
83
- access_token: string;
84
- token_type: 'bearer';
85
- expires_in: number;
86
- }
87
-
88
- export interface Profile {
89
- id: string;
90
- email: string;
91
- firstName: string;
92
- lastName: string;
93
- }
94
-
95
- export interface Index {
96
- name: string;
97
- value: string;
98
- }
99
-
100
- export interface DatabaseDriver {
101
- get(namespace: string, key: string): Promise<any>;
102
- put(
103
- namespace: string,
104
- key: string,
105
- val: any,
106
- ttl: number,
107
- ...indexes: Index[]
108
- ): Promise<any>;
109
- delete(namespace: string, key: string): Promise<any>;
110
- getByIndex(namespace: string, idx: Index): Promise<any>;
111
- }
112
-
113
- export interface Storable {
114
- get(key: string): Promise<any>;
115
- put(key: string, val: any, ...indexes: Index[]): Promise<any>;
116
- delete(key: string): Promise<any>;
117
- getByIndex(idx: Index): Promise<any>;
118
- }
119
-
120
- export interface Encrypted {
121
- iv?: string;
122
- tag?: string;
123
- value: string;
124
- }
125
-
126
- export type EncryptionKey = any;
127
-
128
- export type DatabaseEngine = 'redis' | 'sql' | 'mongo' | 'mem';
129
-
130
- export type DatabaseType = 'postgres' | 'cockroachdb' | 'mysql' | 'mariadb';
131
-
132
- export interface DatabaseOption {
133
- engine: DatabaseEngine;
134
- url: string;
135
- type: DatabaseType;
136
- ttl: number;
137
- cleanupLimit: number;
138
- encryptionKey: string;
139
- }
140
-
141
- export interface SAMLReq {
142
- ssoUrl?: string;
143
- entityID: string;
144
- callbackUrl: string;
145
- isPassive?: boolean;
146
- forceAuthn?: boolean;
147
- identifierFormat?: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress';
148
- providerName?: 'BoxyHQ';
149
- signingKey: string;
150
- }
151
-
152
- export interface SAMLProfile {
153
- audience: string;
154
- claims: Record<string, any>;
155
- issuer: string;
156
- sessionIndex: string;
157
- }
158
-
159
- export interface JacksonOption {
160
- externalUrl: string;
161
- samlPath: string;
162
- samlAudience: string;
163
- preLoadedConfig?: string;
164
- idpEnabled?: boolean;
165
- db: DatabaseOption;
166
- }
167
- }
@@ -1,6 +0,0 @@
1
-
2
- {
3
- "extends": "./tsconfig.json",
4
- "exclude": ["node_modules", "**/test/*"],
5
- }
6
-
package/tsconfig.json DELETED
@@ -1,26 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "outDir": "./dist",
4
- "allowJs": true,
5
- "module": "CommonJS",
6
- "target": "es6", //same as es2015
7
- "forceConsistentCasingInFileNames": true,
8
- "noImplicitAny": false,
9
- "strict": true,
10
- "noImplicitThis": false,
11
- "resolveJsonModule": true,
12
- "esModuleInterop": true,
13
- "declaration": true,
14
- "noEmitOnError": false,
15
- "noUnusedParameters": true,
16
- "removeComments": false,
17
- "strictNullChecks": true,
18
- "allowSyntheticDefaultImports": true,
19
- "experimentalDecorators": true
20
- },
21
- "include": ["./src/**/*"],
22
- "exclude": ["node_modules"],
23
- "ts-node": {
24
- "files": true
25
- }
26
- }