@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
package/src/index.ts DELETED
@@ -1,84 +0,0 @@
1
- import { JacksonOption } from 'saml-jackson';
2
- import { SAMLConfig } from './controller/api';
3
- import { OAuthController } from './controller/oauth';
4
- import DB from './db/db';
5
- import readConfig from './read-config';
6
-
7
- const defaultOpts = (opts: JacksonOption): JacksonOption => {
8
- const newOpts = {
9
- ...opts,
10
- };
11
-
12
- if (!newOpts.externalUrl) {
13
- throw new Error('externalUrl is required');
14
- }
15
-
16
- if (!newOpts.samlPath) {
17
- throw new Error('samlPath is required');
18
- }
19
-
20
- newOpts.samlAudience = newOpts.samlAudience || 'https://saml.boxyhq.com';
21
- newOpts.preLoadedConfig = newOpts.preLoadedConfig || ''; // path to folder containing static SAML config that will be preloaded. This is useful for self-hosted deployments that only have to support a single tenant (or small number of known tenants).
22
- newOpts.idpEnabled = newOpts.idpEnabled === true;
23
-
24
- newOpts.db = newOpts.db || {};
25
- newOpts.db.engine = newOpts.db.engine || 'sql';
26
- newOpts.db.url =
27
- newOpts.db.url || 'postgresql://postgres:postgres@localhost:5432/postgres';
28
- newOpts.db.type = newOpts.db.type || 'postgres'; // Only needed if DB_ENGINE is sql.
29
- newOpts.db.ttl = (newOpts.db.ttl || 300) * 1; // TTL for the code, session and token stores (in seconds)
30
- newOpts.db.cleanupLimit = (newOpts.db.cleanupLimit || 1000) * 1; // Limit cleanup of TTL entries to this many items at a time
31
-
32
- return newOpts;
33
- };
34
-
35
- const controllers = async (
36
- opts: JacksonOption
37
- ): Promise<{
38
- apiController: SAMLConfig;
39
- oauthController: OAuthController;
40
- }> => {
41
- opts = defaultOpts(opts);
42
-
43
- const db = await DB.new(opts.db);
44
-
45
- const configStore = db.store('saml:config');
46
- const sessionStore = db.store('oauth:session', opts.db.ttl);
47
- const codeStore = db.store('oauth:code', opts.db.ttl);
48
- const tokenStore = db.store('oauth:token', opts.db.ttl);
49
-
50
- const apiController = new SAMLConfig({ configStore });
51
-
52
- const oauthController = new OAuthController({
53
- configStore,
54
- sessionStore,
55
- codeStore,
56
- tokenStore,
57
- opts,
58
- });
59
-
60
- // write pre-loaded config if present
61
- if (opts.preLoadedConfig && opts.preLoadedConfig.length > 0) {
62
- const configs = await readConfig(opts.preLoadedConfig);
63
-
64
- for (const config of configs) {
65
- await apiController.config(config);
66
-
67
- console.log(
68
- `loaded config for tenant "${config.tenant}" and product "${config.product}"`
69
- );
70
- }
71
- }
72
-
73
- const type =
74
- opts.db.engine === 'sql' && opts.db.type ? ' Type: ' + opts.db.type : '';
75
-
76
- console.log(`Using engine: ${opts.db.engine}.${type}`);
77
-
78
- return {
79
- apiController,
80
- oauthController,
81
- };
82
- };
83
-
84
- export default controllers;
package/src/jackson.ts DELETED
@@ -1,173 +0,0 @@
1
- import cors from 'cors';
2
- import express from 'express';
3
- import { IOAuthController, ISAMLConfig } from 'saml-jackson';
4
- import { JacksonError } from './controller/error';
5
- import { extractAuthToken } from './controller/utils';
6
- import env from './env';
7
-
8
- //import jackson from './index';
9
-
10
- const jackson = require('./index');
11
-
12
- let apiController: ISAMLConfig;
13
- let oauthController: IOAuthController;
14
-
15
- const oauthPath = '/oauth';
16
- const apiPath = '/api/v1/saml';
17
-
18
- const app = express();
19
-
20
- app.use(express.json());
21
- app.use(express.urlencoded({ extended: true }));
22
-
23
- app.get(oauthPath + '/authorize', async (req, res) => {
24
- try {
25
- const { redirect_url } = await oauthController.authorize(req.query);
26
-
27
- res.redirect(redirect_url);
28
- } catch (err) {
29
- const { message, statusCode = 500 } = err as JacksonError;
30
-
31
- res.status(statusCode).send(message);
32
- }
33
- });
34
-
35
- app.post(env.samlPath, async (req, res) => {
36
- try {
37
- const { redirect_url } = await oauthController.samlResponse(req.body);
38
-
39
- res.redirect(redirect_url);
40
- } catch (err) {
41
- const { message, statusCode = 500 } = err as JacksonError;
42
-
43
- res.status(statusCode).send(message);
44
- }
45
- });
46
-
47
- app.post(oauthPath + '/token', cors(), async (req, res) => {
48
- try {
49
- const result = await oauthController.token(req.body);
50
-
51
- res.json(result);
52
- } catch (err) {
53
- const { message, statusCode = 500 } = err as JacksonError;
54
-
55
- res.status(statusCode).send(message);
56
- }
57
- });
58
-
59
- app.get(oauthPath + '/userinfo', async (req, res) => {
60
- try {
61
- let token = extractAuthToken(req);
62
-
63
- // check for query param
64
- if (!token) {
65
- token = req.query.access_token;
66
- }
67
-
68
- if (!token) {
69
- return res.status(401).json({ message: 'Unauthorized' });
70
- }
71
-
72
- const profile = await oauthController.userInfo(token);
73
-
74
- res.json(profile);
75
- } catch (err) {
76
- const { message, statusCode = 500 } = err as JacksonError;
77
-
78
- res.status(statusCode).json({ message });
79
- }
80
- });
81
-
82
- const server = app.listen(env.hostPort, async () => {
83
- console.log(
84
- `🚀 The path of the righteous server: http://${env.hostUrl}:${env.hostPort}`
85
- );
86
-
87
- const ctrlrModule = await jackson(env);
88
-
89
- apiController = ctrlrModule.apiController;
90
- oauthController = ctrlrModule.oauthController;
91
- });
92
-
93
- // Internal routes, recommended not to expose this to the public interface though it would be guarded by API key(s)
94
- let internalApp = app;
95
-
96
- if (env.useInternalServer) {
97
- internalApp = express();
98
-
99
- internalApp.use(express.json());
100
- internalApp.use(express.urlencoded({ extended: true }));
101
- }
102
-
103
- const validateApiKey = (token) => {
104
- return env.apiKeys.includes(token);
105
- };
106
-
107
- internalApp.post(apiPath + '/config', async (req, res) => {
108
- try {
109
- const apiKey = extractAuthToken(req);
110
- if (!validateApiKey(apiKey)) {
111
- res.status(401).send('Unauthorized');
112
- return;
113
- }
114
-
115
- res.json(await apiController.config(req.body));
116
- } catch (err) {
117
- const { message } = err as JacksonError;
118
-
119
- res.status(500).json({
120
- error: message,
121
- });
122
- }
123
- });
124
-
125
- internalApp.get(apiPath + '/config', async (req, res) => {
126
- try {
127
- const apiKey = extractAuthToken(req);
128
- if (!validateApiKey(apiKey)) {
129
- res.status(401).send('Unauthorized');
130
- return;
131
- }
132
-
133
- res.json(await apiController.getConfig(req.query));
134
- } catch (err) {
135
- const { message } = err as JacksonError;
136
-
137
- res.status(500).json({
138
- error: message,
139
- });
140
- }
141
- });
142
-
143
- internalApp.delete(apiPath + '/config', async (req, res) => {
144
- try {
145
- const apiKey = extractAuthToken(req);
146
- if (!validateApiKey(apiKey)) {
147
- res.status(401).send('Unauthorized');
148
- return;
149
- }
150
- await apiController.deleteConfig(req.body);
151
- res.status(200).end();
152
- } catch (err) {
153
- const { message } = err as JacksonError;
154
-
155
- res.status(500).json({
156
- error: message,
157
- });
158
- }
159
- });
160
-
161
- let internalServer = server;
162
- if (env.useInternalServer) {
163
- internalServer = internalApp.listen(env.internalHostPort, async () => {
164
- console.log(
165
- `🚀 The path of the righteous internal server: http://${env.internalHostUrl}:${env.internalHostPort}`
166
- );
167
- });
168
- }
169
-
170
- module.exports = {
171
- server,
172
- internalServer,
173
- };
@@ -1,29 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { IdPConfig } from 'saml-jackson';
4
-
5
- const readConfig = async (preLoadedConfig: string): Promise<IdPConfig[]> => {
6
- if (preLoadedConfig.startsWith('./')) {
7
- preLoadedConfig = path.resolve(process.cwd(), preLoadedConfig);
8
- }
9
-
10
- const files = await fs.promises.readdir(preLoadedConfig);
11
- const configs: IdPConfig[] = [];
12
-
13
- for (let idx in files) {
14
- const file = files[idx];
15
- if (file.endsWith('.js')) {
16
- const config = require(path.join(preLoadedConfig, file)) as IdPConfig;
17
- const rawMetadata = await fs.promises.readFile(
18
- path.join(preLoadedConfig, path.parse(file).name + '.xml'),
19
- 'utf8'
20
- );
21
- config.rawMetadata = rawMetadata;
22
- configs.push(config);
23
- }
24
- }
25
-
26
- return configs;
27
- };
28
-
29
- export default readConfig;
@@ -1,41 +0,0 @@
1
- const mapping = [
2
- {
3
- attribute: 'id',
4
- schema:
5
- 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier',
6
- },
7
- {
8
- attribute: 'email',
9
- schema:
10
- 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
11
- },
12
- {
13
- attribute: 'firstName',
14
- schema: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname',
15
- },
16
- {
17
- attribute: 'lastName',
18
- schema: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname',
19
- },
20
- ] as const;
21
-
22
- type attributes = typeof mapping[number]['attribute'];
23
- type schemas = typeof mapping[number]['schema'];
24
-
25
- const map = (claims: Record<attributes | schemas, unknown>) => {
26
- const profile = {
27
- raw: claims,
28
- };
29
-
30
- mapping.forEach((m) => {
31
- if (claims[m.attribute]) {
32
- profile[m.attribute] = claims[m.attribute];
33
- } else if (claims[m.schema]) {
34
- profile[m.attribute] = claims[m.schema];
35
- }
36
- });
37
-
38
- return profile;
39
- };
40
-
41
- export default { map };
package/src/saml/saml.ts DELETED
@@ -1,233 +0,0 @@
1
- import saml from '@boxyhq/saml20';
2
- import xml2js from 'xml2js';
3
- import thumbprint from 'thumbprint';
4
- import xmlcrypto from 'xml-crypto';
5
- import * as rambda from 'rambda';
6
- import xmlbuilder from 'xmlbuilder';
7
- import crypto from 'crypto';
8
- import claims from './claims';
9
- import { SAMLProfile, SAMLReq } from 'saml-jackson';
10
-
11
- const idPrefix = '_';
12
- const authnXPath =
13
- '/*[local-name(.)="AuthnRequest" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:protocol"]';
14
- const issuerXPath =
15
- '/*[local-name(.)="Issuer" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:assertion"]';
16
-
17
- const signRequest = (xml: string, signingKey: string) => {
18
- if (!xml) {
19
- throw new Error('Please specify xml');
20
- }
21
- if (!signingKey) {
22
- throw new Error('Please specify signingKey');
23
- }
24
-
25
- const sig = new xmlcrypto.SignedXml();
26
- sig.signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
27
- sig.signingKey = signingKey;
28
- sig.addReference(
29
- authnXPath,
30
- [
31
- 'http://www.w3.org/2000/09/xmldsig#enveloped-signature',
32
- 'http://www.w3.org/2001/10/xml-exc-c14n#',
33
- ],
34
- 'http://www.w3.org/2001/04/xmlenc#sha256'
35
- );
36
- sig.computeSignature(xml, {
37
- location: { reference: authnXPath + issuerXPath, action: 'after' },
38
- });
39
-
40
- return sig.getSignedXml();
41
- };
42
-
43
- const request = ({
44
- ssoUrl,
45
- entityID,
46
- callbackUrl,
47
- isPassive = false,
48
- forceAuthn = false,
49
- identifierFormat = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
50
- providerName = 'BoxyHQ',
51
- signingKey,
52
- }: SAMLReq): { id: string; request: string } => {
53
- const id = idPrefix + crypto.randomBytes(10).toString('hex');
54
- const date = new Date().toISOString();
55
-
56
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
- const samlReq: Record<string, any> = {
58
- 'samlp:AuthnRequest': {
59
- '@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
60
- '@ID': id,
61
- '@Version': '2.0',
62
- '@IssueInstant': date,
63
- '@ProtocolBinding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
64
- '@Destination': ssoUrl,
65
- 'saml:Issuer': {
66
- '@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
67
- '#text': entityID,
68
- },
69
- },
70
- };
71
-
72
- if (isPassive) samlReq['samlp:AuthnRequest']['@IsPassive'] = true;
73
-
74
- if (forceAuthn) {
75
- samlReq['samlp:AuthnRequest']['@ForceAuthn'] = true;
76
- }
77
-
78
- samlReq['samlp:AuthnRequest']['@AssertionConsumerServiceURL'] = callbackUrl;
79
-
80
- samlReq['samlp:AuthnRequest']['samlp:NameIDPolicy'] = {
81
- '@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
82
- '@Format': identifierFormat,
83
- '@AllowCreate': 'true',
84
- };
85
-
86
- if (providerName != null) {
87
- samlReq['samlp:AuthnRequest']['@ProviderName'] = providerName;
88
- }
89
-
90
- let xml = xmlbuilder.create(samlReq).end({});
91
- if (signingKey) {
92
- xml = signRequest(xml, signingKey);
93
- }
94
-
95
- return {
96
- id,
97
- request: xml,
98
- };
99
- };
100
-
101
- const parseAsync = async (rawAssertion: string): Promise<SAMLProfile> => {
102
- return new Promise((resolve, reject) => {
103
- saml.parse(
104
- rawAssertion,
105
- function onParseAsync(err: Error, profile: SAMLProfile) {
106
- if (err) {
107
- reject(err);
108
- return;
109
- }
110
-
111
- resolve(profile);
112
- }
113
- );
114
- });
115
- };
116
-
117
- const validateAsync = async (
118
- rawAssertion: string,
119
- options
120
- ): Promise<SAMLProfile> => {
121
- return new Promise((resolve, reject) => {
122
- saml.validate(
123
- rawAssertion,
124
- options,
125
- function onValidateAsync(err, profile: SAMLProfile) {
126
- if (err) {
127
- reject(err);
128
- return;
129
- }
130
-
131
- if (profile && profile.claims) {
132
- // we map claims to our attributes id, email, firstName, lastName where possible. We also map original claims to raw
133
- profile.claims = claims.map(profile.claims);
134
-
135
- // some providers don't return the id in the assertion, we set it to a sha256 hash of the email
136
- if (!profile.claims.id) {
137
- profile.claims.id = crypto
138
- .createHash('sha256')
139
- .update(profile.claims.email)
140
- .digest('hex');
141
- }
142
- }
143
-
144
- resolve(profile);
145
- }
146
- );
147
- });
148
- };
149
-
150
- const parseMetadataAsync = async (
151
- idpMeta: string
152
- ): Promise<Record<string, any>> => {
153
- return new Promise((resolve, reject) => {
154
- xml2js.parseString(
155
- idpMeta,
156
- { tagNameProcessors: [xml2js.processors.stripPrefix] },
157
- (err: Error, res) => {
158
- if (err) {
159
- reject(err);
160
- return;
161
- }
162
-
163
- const entityID = rambda.path('EntityDescriptor.$.entityID', res);
164
- let X509Certificate = null;
165
- let ssoPostUrl: null | undefined = null;
166
- let ssoRedirectUrl: null | undefined = null;
167
- let loginType = 'idp';
168
-
169
- let ssoDes: any = rambda.pathOr(
170
- null,
171
- 'EntityDescriptor.IDPSSODescriptor',
172
- res
173
- );
174
- if (!ssoDes) {
175
- ssoDes = rambda.pathOr([], 'EntityDescriptor.SPSSODescriptor', res);
176
- if (!ssoDes) {
177
- loginType = 'sp';
178
- }
179
- }
180
-
181
- for (const ssoDesRec of ssoDes) {
182
- const keyDes = ssoDesRec['KeyDescriptor'];
183
- for (const keyDesRec of keyDes) {
184
- if (keyDesRec['$'] && keyDesRec['$'].use === 'signing') {
185
- const ki = keyDesRec['KeyInfo'][0];
186
- const cd = ki['X509Data'][0];
187
- X509Certificate = cd['X509Certificate'][0];
188
- }
189
- }
190
-
191
- const ssoSvc =
192
- ssoDesRec['SingleSignOnService'] ||
193
- ssoDesRec['AssertionConsumerService'] ||
194
- [];
195
- for (const ssoSvcRec of ssoSvc) {
196
- if (
197
- rambda.pathOr('', '$.Binding', ssoSvcRec).endsWith('HTTP-POST')
198
- ) {
199
- ssoPostUrl = rambda.path('$.Location', ssoSvcRec);
200
- } else if (
201
- rambda
202
- .pathOr('', '$.Binding', ssoSvcRec)
203
- .endsWith('HTTP-Redirect')
204
- ) {
205
- ssoRedirectUrl = rambda.path('$.Location', ssoSvcRec);
206
- }
207
- }
208
- }
209
-
210
- const ret: Record<string, any> = {
211
- sso: {},
212
- };
213
- if (entityID) {
214
- ret.entityID = entityID;
215
- }
216
- if (X509Certificate) {
217
- ret.thumbprint = thumbprint.calculate(X509Certificate);
218
- }
219
- if (ssoPostUrl) {
220
- ret.sso.postUrl = ssoPostUrl;
221
- }
222
- if (ssoRedirectUrl) {
223
- ret.sso.redirectUrl = ssoRedirectUrl;
224
- }
225
- ret.loginType = loginType;
226
-
227
- resolve(ret);
228
- }
229
- );
230
- });
231
- };
232
-
233
- export default { request, parseAsync, validateAsync, parseMetadataAsync };
package/src/saml/x509.ts DELETED
@@ -1,51 +0,0 @@
1
- import * as x509 from '@peculiar/x509';
2
- import { Crypto } from '@peculiar/webcrypto';
3
-
4
- const crypto = new Crypto();
5
- x509.cryptoProvider.set(crypto);
6
-
7
- const alg = {
8
- name: 'RSASSA-PKCS1-v1_5',
9
- hash: 'SHA-256',
10
- publicExponent: new Uint8Array([1, 0, 1]),
11
- modulusLength: 2048,
12
- };
13
-
14
- const generate = async () => {
15
- const keys = await crypto.subtle.generateKey(alg, true, ['sign', 'verify']);
16
-
17
- const extensions: x509.Extension[] = [
18
- new x509.BasicConstraintsExtension(false, undefined, true),
19
- ];
20
-
21
- extensions.push(
22
- new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature, true)
23
- );
24
- if (keys.publicKey) {
25
- extensions.push(
26
- await x509.SubjectKeyIdentifierExtension.create(keys.publicKey)
27
- );
28
- }
29
-
30
- const cert = await x509.X509CertificateGenerator.createSelfSigned({
31
- serialNumber: '01',
32
- name: 'CN=BoxyHQ Jackson',
33
- notBefore: new Date(),
34
- notAfter: new Date('3021/01/01'), // TODO: set shorter expiry and rotate ceritifcates
35
- signingAlgorithm: alg,
36
- keys: keys,
37
- extensions,
38
- });
39
- if (keys.privateKey) {
40
- const pkcs8 = await crypto.subtle.exportKey('pkcs8', keys.privateKey);
41
-
42
- return {
43
- publicKey: cert.toString('pem'),
44
- privateKey: x509.PemConverter.encode(pkcs8, 'private key'),
45
- };
46
- }
47
- };
48
-
49
- export default {
50
- generate,
51
- };