@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
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
- };