@boxyhq/saml-jackson 0.2.3-beta.177 → 0.2.3-beta.206
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.
- package/ nodemon.json +12 -0
- package/.nyc_output/3fcecc4f-446a-412f-b692-364d37085e9c.json +1 -0
- package/.nyc_output/5ae45aaa-c2ec-465f-be5d-1eb335276bfd.json +1 -0
- package/.nyc_output/b5c9facc-f405-4aff-9bd6-619d3482da3d.json +1 -0
- package/.nyc_output/processinfo/3fcecc4f-446a-412f-b692-364d37085e9c.json +1 -0
- package/.nyc_output/processinfo/5ae45aaa-c2ec-465f-be5d-1eb335276bfd.json +1 -0
- package/.nyc_output/processinfo/b5c9facc-f405-4aff-9bd6-619d3482da3d.json +1 -0
- package/.nyc_output/processinfo/index.json +1 -0
- package/package.json +23 -15
- package/.eslintrc.js +0 -13
- package/prettier.config.js +0 -4
- package/src/controller/api.js +0 -167
- package/src/controller/error.js +0 -12
- package/src/controller/oauth/allowed.js +0 -19
- package/src/controller/oauth/code-verifier.js +0 -16
- package/src/controller/oauth/redirect.js +0 -18
- package/src/controller/oauth.js +0 -321
- package/src/controller/utils.js +0 -19
- package/src/db/db.js +0 -81
- package/src/db/db.test.js +0 -302
- package/src/db/encrypter.js +0 -36
- package/src/db/mem.js +0 -111
- package/src/db/mongo.js +0 -89
- package/src/db/redis.js +0 -88
- package/src/db/sql/entity/JacksonIndex.js +0 -42
- package/src/db/sql/entity/JacksonStore.js +0 -42
- package/src/db/sql/entity/JacksonTTL.js +0 -23
- package/src/db/sql/model/JacksonIndex.js +0 -9
- package/src/db/sql/model/JacksonStore.js +0 -10
- package/src/db/sql/model/JacksonTTL.js +0 -8
- package/src/db/sql/sql.js +0 -153
- package/src/db/store.js +0 -42
- package/src/db/utils.js +0 -30
- package/src/env.js +0 -39
- package/src/index.js +0 -67
- package/src/jackson.js +0 -161
- package/src/read-config.js +0 -24
- package/src/saml/claims.js +0 -40
- package/src/saml/saml.js +0 -223
- package/src/saml/x509.js +0 -48
- package/src/test/api.test.js +0 -186
- package/src/test/data/metadata/boxyhq.js +0 -6
- package/src/test/data/metadata/boxyhq.xml +0 -30
- package/src/test/data/saml_response +0 -1
- package/src/test/oauth.test.js +0 -342
package/src/jackson.js
DELETED
@@ -1,161 +0,0 @@
|
|
1
|
-
const express = require('express');
|
2
|
-
const cors = require('cors');
|
3
|
-
|
4
|
-
const env = require('./env.js');
|
5
|
-
const { extractAuthToken } = require('./controller/utils.js');
|
6
|
-
|
7
|
-
let apiController;
|
8
|
-
let oauthController;
|
9
|
-
|
10
|
-
const oauthPath = '/oauth';
|
11
|
-
const apiPath = '/api/v1/saml';
|
12
|
-
|
13
|
-
const app = express();
|
14
|
-
|
15
|
-
app.use(express.json());
|
16
|
-
app.use(express.urlencoded({ extended: true }));
|
17
|
-
|
18
|
-
app.get(oauthPath + '/authorize', async (req, res) => {
|
19
|
-
try {
|
20
|
-
const { redirect_url } = await oauthController.authorize(req.query);
|
21
|
-
|
22
|
-
res.redirect(redirect_url);
|
23
|
-
} catch (err) {
|
24
|
-
const { message, statusCode = 500 } = err;
|
25
|
-
|
26
|
-
res.status(statusCode).send(message);
|
27
|
-
}
|
28
|
-
});
|
29
|
-
|
30
|
-
app.post(env.samlPath, async (req, res) => {
|
31
|
-
try {
|
32
|
-
const { redirect_url } = await oauthController.samlResponse(req.body);
|
33
|
-
|
34
|
-
res.redirect(redirect_url);
|
35
|
-
} catch (err) {
|
36
|
-
const { message, statusCode = 500 } = err;
|
37
|
-
|
38
|
-
res.status(statusCode).send(message);
|
39
|
-
}
|
40
|
-
});
|
41
|
-
|
42
|
-
app.post(oauthPath + '/token', cors(), async (req, res) => {
|
43
|
-
try {
|
44
|
-
const result = await oauthController.token(req.body);
|
45
|
-
|
46
|
-
res.json(result);
|
47
|
-
} catch (err) {
|
48
|
-
const { message, statusCode = 500 } = err;
|
49
|
-
|
50
|
-
res.status(statusCode).send(message);
|
51
|
-
}
|
52
|
-
});
|
53
|
-
|
54
|
-
app.get(oauthPath + '/userinfo', async (req, res) => {
|
55
|
-
try {
|
56
|
-
let token = extractAuthToken(req);
|
57
|
-
|
58
|
-
// check for query param
|
59
|
-
if (!token) {
|
60
|
-
token = req.query.access_token;
|
61
|
-
}
|
62
|
-
|
63
|
-
if (!token) {
|
64
|
-
res.status(401).json({ message: 'Unauthorized' });
|
65
|
-
}
|
66
|
-
|
67
|
-
const profile = await oauthController.userInfo(token);
|
68
|
-
|
69
|
-
res.json(profile);
|
70
|
-
} catch (err) {
|
71
|
-
const { message, statusCode = 500 } = err;
|
72
|
-
|
73
|
-
res.status(statusCode).json({ message });
|
74
|
-
}
|
75
|
-
});
|
76
|
-
|
77
|
-
const server = app.listen(env.hostPort, async () => {
|
78
|
-
console.log(
|
79
|
-
`🚀 The path of the righteous server: http://${env.hostUrl}:${env.hostPort}`
|
80
|
-
);
|
81
|
-
|
82
|
-
const ret = await require('./index.js')(env);
|
83
|
-
apiController = ret.apiController;
|
84
|
-
oauthController = ret.oauthController;
|
85
|
-
});
|
86
|
-
|
87
|
-
// Internal routes, recommended not to expose this to the public interface though it would be guarded by API key(s)
|
88
|
-
let internalApp = app;
|
89
|
-
|
90
|
-
if (env.useInternalServer) {
|
91
|
-
internalApp = express();
|
92
|
-
|
93
|
-
internalApp.use(express.json());
|
94
|
-
internalApp.use(express.urlencoded({ extended: true }));
|
95
|
-
}
|
96
|
-
|
97
|
-
const validateApiKey = (token) => {
|
98
|
-
return env.apiKeys.includes(token);
|
99
|
-
};
|
100
|
-
|
101
|
-
internalApp.post(apiPath + '/config', async (req, res) => {
|
102
|
-
try {
|
103
|
-
const apiKey = extractAuthToken(req);
|
104
|
-
if (!validateApiKey(apiKey)) {
|
105
|
-
res.status(401).send('Unauthorized');
|
106
|
-
return;
|
107
|
-
}
|
108
|
-
|
109
|
-
res.json(await apiController.config(req.body));
|
110
|
-
} catch (err) {
|
111
|
-
res.status(500).json({
|
112
|
-
error: err.message,
|
113
|
-
});
|
114
|
-
}
|
115
|
-
});
|
116
|
-
|
117
|
-
internalApp.get(apiPath + '/config', async (req, res) => {
|
118
|
-
try {
|
119
|
-
const apiKey = extractAuthToken(req);
|
120
|
-
if (!validateApiKey(apiKey)) {
|
121
|
-
res.status(401).send('Unauthorized');
|
122
|
-
return;
|
123
|
-
}
|
124
|
-
|
125
|
-
res.json(await apiController.getConfig(req.query));
|
126
|
-
} catch (err) {
|
127
|
-
res.status(500).json({
|
128
|
-
error: err.message,
|
129
|
-
});
|
130
|
-
}
|
131
|
-
});
|
132
|
-
|
133
|
-
internalApp.delete(apiPath + '/config', async (req, res) => {
|
134
|
-
try {
|
135
|
-
const apiKey = extractAuthToken(req);
|
136
|
-
if (!validateApiKey(apiKey)) {
|
137
|
-
res.status(401).send('Unauthorized');
|
138
|
-
return;
|
139
|
-
}
|
140
|
-
await apiController.deleteConfig(req.body);
|
141
|
-
res.status(200).end();
|
142
|
-
} catch (err) {
|
143
|
-
res.status(500).json({
|
144
|
-
error: err.message,
|
145
|
-
});
|
146
|
-
}
|
147
|
-
});
|
148
|
-
|
149
|
-
let internalServer = server;
|
150
|
-
if (env.useInternalServer) {
|
151
|
-
internalServer = internalApp.listen(env.internalHostPort, async () => {
|
152
|
-
console.log(
|
153
|
-
`🚀 The path of the righteous internal server: http://${env.internalHostUrl}:${env.internalHostPort}`
|
154
|
-
);
|
155
|
-
});
|
156
|
-
}
|
157
|
-
|
158
|
-
module.exports = {
|
159
|
-
server,
|
160
|
-
internalServer,
|
161
|
-
};
|
package/src/read-config.js
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
const fs = require('fs');
|
2
|
-
const path = require('path');
|
3
|
-
|
4
|
-
module.exports = async function (preLoadedConfig) {
|
5
|
-
if (preLoadedConfig.startsWith('./')) {
|
6
|
-
preLoadedConfig = path.resolve(process.cwd(), preLoadedConfig);
|
7
|
-
}
|
8
|
-
const files = await fs.promises.readdir(preLoadedConfig);
|
9
|
-
const configs = [];
|
10
|
-
for (let idx in files) {
|
11
|
-
const file = files[idx];
|
12
|
-
if (file.endsWith('.js')) {
|
13
|
-
const config = require(path.join(preLoadedConfig, file));
|
14
|
-
const rawMetadata = await fs.promises.readFile(
|
15
|
-
path.join(preLoadedConfig, path.parse(file).name + '.xml'),
|
16
|
-
'utf8'
|
17
|
-
);
|
18
|
-
config.rawMetadata = rawMetadata;
|
19
|
-
configs.push(config);
|
20
|
-
}
|
21
|
-
}
|
22
|
-
|
23
|
-
return configs;
|
24
|
-
};
|
package/src/saml/claims.js
DELETED
@@ -1,40 +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
|
-
];
|
21
|
-
|
22
|
-
const map = (claims) => {
|
23
|
-
const profile = {
|
24
|
-
raw: claims,
|
25
|
-
};
|
26
|
-
|
27
|
-
mapping.forEach((m) => {
|
28
|
-
if (claims[m.attribute]) {
|
29
|
-
profile[m.attribute] = claims[m.attribute];
|
30
|
-
} else if (claims[m.schema]) {
|
31
|
-
profile[m.attribute] = claims[m.schema];
|
32
|
-
}
|
33
|
-
});
|
34
|
-
|
35
|
-
return profile;
|
36
|
-
};
|
37
|
-
|
38
|
-
module.exports = {
|
39
|
-
map,
|
40
|
-
};
|
package/src/saml/saml.js
DELETED
@@ -1,223 +0,0 @@
|
|
1
|
-
const saml = require('@boxyhq/saml20');
|
2
|
-
const xml2js = require('xml2js');
|
3
|
-
const rambda = require('rambda');
|
4
|
-
const thumbprint = require('thumbprint');
|
5
|
-
const xmlbuilder = require('xmlbuilder');
|
6
|
-
const crypto = require('crypto');
|
7
|
-
const xmlcrypto = require('xml-crypto');
|
8
|
-
const claims = require('./claims');
|
9
|
-
|
10
|
-
const idPrefix = '_';
|
11
|
-
const authnXPath =
|
12
|
-
'/*[local-name(.)="AuthnRequest" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:protocol"]';
|
13
|
-
const issuerXPath =
|
14
|
-
'/*[local-name(.)="Issuer" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:assertion"]';
|
15
|
-
|
16
|
-
const signRequest = (xml, signingKey) => {
|
17
|
-
if (!xml) {
|
18
|
-
throw new Error('Please specify xml');
|
19
|
-
}
|
20
|
-
if (!signingKey) {
|
21
|
-
throw new Error('Please specify signingKey');
|
22
|
-
}
|
23
|
-
|
24
|
-
const sig = new xmlcrypto.SignedXml();
|
25
|
-
sig.signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
|
26
|
-
sig.signingKey = signingKey;
|
27
|
-
sig.addReference(
|
28
|
-
authnXPath,
|
29
|
-
[
|
30
|
-
'http://www.w3.org/2000/09/xmldsig#enveloped-signature',
|
31
|
-
'http://www.w3.org/2001/10/xml-exc-c14n#',
|
32
|
-
],
|
33
|
-
'http://www.w3.org/2001/04/xmlenc#sha256'
|
34
|
-
);
|
35
|
-
sig.computeSignature(xml, {
|
36
|
-
location: { reference: authnXPath + issuerXPath, action: 'after' },
|
37
|
-
});
|
38
|
-
|
39
|
-
return sig.getSignedXml();
|
40
|
-
};
|
41
|
-
|
42
|
-
module.exports = {
|
43
|
-
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
|
-
}) => {
|
53
|
-
const id = idPrefix + crypto.randomBytes(10).toString('hex');
|
54
|
-
const date = new Date().toISOString();
|
55
|
-
|
56
|
-
const samlReq = {
|
57
|
-
'samlp:AuthnRequest': {
|
58
|
-
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
|
59
|
-
'@ID': id,
|
60
|
-
'@Version': '2.0',
|
61
|
-
'@IssueInstant': date,
|
62
|
-
'@ProtocolBinding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
|
63
|
-
'@Destination': ssoUrl,
|
64
|
-
'saml:Issuer': {
|
65
|
-
'@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
|
66
|
-
'#text': entityID,
|
67
|
-
},
|
68
|
-
},
|
69
|
-
};
|
70
|
-
|
71
|
-
if (isPassive) samlReq['samlp:AuthnRequest']['@IsPassive'] = true;
|
72
|
-
|
73
|
-
if (forceAuthn) {
|
74
|
-
samlReq['samlp:AuthnRequest']['@ForceAuthn'] = true;
|
75
|
-
}
|
76
|
-
|
77
|
-
samlReq['samlp:AuthnRequest']['@AssertionConsumerServiceURL'] = callbackUrl;
|
78
|
-
|
79
|
-
samlReq['samlp:AuthnRequest']['samlp:NameIDPolicy'] = {
|
80
|
-
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
|
81
|
-
'@Format': identifierFormat,
|
82
|
-
'@AllowCreate': 'true',
|
83
|
-
};
|
84
|
-
|
85
|
-
if (providerName != null) {
|
86
|
-
samlReq['samlp:AuthnRequest']['@ProviderName'] = providerName;
|
87
|
-
}
|
88
|
-
|
89
|
-
let xml = xmlbuilder.create(samlReq).end({});
|
90
|
-
if (signingKey) {
|
91
|
-
xml = signRequest(xml, signingKey);
|
92
|
-
}
|
93
|
-
|
94
|
-
return {
|
95
|
-
id,
|
96
|
-
request: xml,
|
97
|
-
};
|
98
|
-
},
|
99
|
-
|
100
|
-
parseAsync: async (rawAssertion) => {
|
101
|
-
return new Promise((resolve, reject) => {
|
102
|
-
saml.parse(rawAssertion, function onParseAsync(err, profile) {
|
103
|
-
if (err) {
|
104
|
-
reject(err);
|
105
|
-
return;
|
106
|
-
}
|
107
|
-
|
108
|
-
resolve(profile);
|
109
|
-
});
|
110
|
-
});
|
111
|
-
},
|
112
|
-
|
113
|
-
validateAsync: async (rawAssertion, options) => {
|
114
|
-
return new Promise((resolve, reject) => {
|
115
|
-
saml.validate(
|
116
|
-
rawAssertion,
|
117
|
-
options,
|
118
|
-
function onValidateAsync(err, profile) {
|
119
|
-
if (err) {
|
120
|
-
reject(err);
|
121
|
-
return;
|
122
|
-
}
|
123
|
-
|
124
|
-
if (profile && profile.claims) {
|
125
|
-
// we map claims to our attributes id, email, firstName, lastName where possible. We also map original claims to raw
|
126
|
-
profile.claims = claims.map(profile.claims);
|
127
|
-
|
128
|
-
// some providers don't return the id in the assertion, we set it to a sha256 hash of the email
|
129
|
-
if (!profile.claims.id) {
|
130
|
-
profile.claims.id = crypto
|
131
|
-
.createHash('sha256')
|
132
|
-
.update(profile.claims.email)
|
133
|
-
.digest('hex');
|
134
|
-
}
|
135
|
-
}
|
136
|
-
|
137
|
-
resolve(profile);
|
138
|
-
}
|
139
|
-
);
|
140
|
-
});
|
141
|
-
},
|
142
|
-
|
143
|
-
parseMetadataAsync: async (idpMeta) => {
|
144
|
-
return new Promise((resolve, reject) => {
|
145
|
-
xml2js.parseString(
|
146
|
-
idpMeta,
|
147
|
-
{ tagNameProcessors: [xml2js.processors.stripPrefix] },
|
148
|
-
(err, res) => {
|
149
|
-
if (err) {
|
150
|
-
reject(err);
|
151
|
-
return;
|
152
|
-
}
|
153
|
-
|
154
|
-
const entityID = rambda.path('EntityDescriptor.$.entityID', res);
|
155
|
-
let X509Certificate = null;
|
156
|
-
let ssoPostUrl = null;
|
157
|
-
let ssoRedirectUrl = null;
|
158
|
-
let loginType = 'idp';
|
159
|
-
|
160
|
-
let ssoDes = rambda.pathOr(
|
161
|
-
null,
|
162
|
-
'EntityDescriptor.IDPSSODescriptor',
|
163
|
-
res
|
164
|
-
);
|
165
|
-
if (!ssoDes) {
|
166
|
-
ssoDes = rambda.pathOr([], 'EntityDescriptor.SPSSODescriptor', res);
|
167
|
-
if (!ssoDes) {
|
168
|
-
loginType = 'sp';
|
169
|
-
}
|
170
|
-
}
|
171
|
-
|
172
|
-
for (const ssoDesRec of ssoDes) {
|
173
|
-
const keyDes = ssoDesRec['KeyDescriptor'];
|
174
|
-
for (const keyDesRec of keyDes) {
|
175
|
-
if (keyDesRec['$'] && keyDesRec['$'].use === 'signing') {
|
176
|
-
const ki = keyDesRec['KeyInfo'][0];
|
177
|
-
const cd = ki['X509Data'][0];
|
178
|
-
X509Certificate = cd['X509Certificate'][0];
|
179
|
-
}
|
180
|
-
}
|
181
|
-
|
182
|
-
const ssoSvc =
|
183
|
-
ssoDesRec['SingleSignOnService'] ||
|
184
|
-
ssoDesRec['AssertionConsumerService'] ||
|
185
|
-
[];
|
186
|
-
for (const ssoSvcRec of ssoSvc) {
|
187
|
-
if (
|
188
|
-
rambda.pathOr('', '$.Binding', ssoSvcRec).endsWith('HTTP-POST')
|
189
|
-
) {
|
190
|
-
ssoPostUrl = rambda.path('$.Location', ssoSvcRec);
|
191
|
-
} else if (
|
192
|
-
rambda
|
193
|
-
.pathOr('', '$.Binding', ssoSvcRec)
|
194
|
-
.endsWith('HTTP-Redirect')
|
195
|
-
) {
|
196
|
-
ssoRedirectUrl = rambda.path('$.Location', ssoSvcRec);
|
197
|
-
}
|
198
|
-
}
|
199
|
-
}
|
200
|
-
|
201
|
-
const ret = {
|
202
|
-
sso: {},
|
203
|
-
};
|
204
|
-
if (entityID) {
|
205
|
-
ret.entityID = entityID;
|
206
|
-
}
|
207
|
-
if (X509Certificate) {
|
208
|
-
ret.thumbprint = thumbprint.calculate(X509Certificate);
|
209
|
-
}
|
210
|
-
if (ssoPostUrl) {
|
211
|
-
ret.sso.postUrl = ssoPostUrl;
|
212
|
-
}
|
213
|
-
if (ssoRedirectUrl) {
|
214
|
-
ret.sso.redirectUrl = ssoRedirectUrl;
|
215
|
-
}
|
216
|
-
ret.loginType = loginType;
|
217
|
-
|
218
|
-
resolve(ret);
|
219
|
-
}
|
220
|
-
);
|
221
|
-
});
|
222
|
-
},
|
223
|
-
};
|
package/src/saml/x509.js
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
const x509 = require('@peculiar/x509');
|
2
|
-
const { Crypto } = require('@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 = [
|
18
|
-
new x509.BasicConstraintsExtension(false, undefined, true),
|
19
|
-
];
|
20
|
-
|
21
|
-
extensions.push(
|
22
|
-
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature, true)
|
23
|
-
);
|
24
|
-
extensions.push(
|
25
|
-
await x509.SubjectKeyIdentifierExtension.create(keys.publicKey)
|
26
|
-
);
|
27
|
-
|
28
|
-
const cert = await x509.X509CertificateGenerator.createSelfSigned({
|
29
|
-
serialNumber: '01',
|
30
|
-
name: 'CN=BoxyHQ Jackson',
|
31
|
-
notBefore: new Date(),
|
32
|
-
notAfter: new Date('3021/01/01'), // TODO: set shorter expiry and rotate ceritifcates
|
33
|
-
signingAlgorithm: alg,
|
34
|
-
keys: keys,
|
35
|
-
extensions,
|
36
|
-
});
|
37
|
-
|
38
|
-
const pkcs8 = await crypto.subtle.exportKey('pkcs8', keys.privateKey);
|
39
|
-
|
40
|
-
return {
|
41
|
-
publicKey: cert.toString('pem'),
|
42
|
-
privateKey: x509.PemConverter.encode(pkcs8, 'private key'),
|
43
|
-
};
|
44
|
-
};
|
45
|
-
|
46
|
-
module.exports = {
|
47
|
-
generate,
|
48
|
-
};
|
package/src/test/api.test.js
DELETED
@@ -1,186 +0,0 @@
|
|
1
|
-
const tap = require('tap');
|
2
|
-
const path = require('path');
|
3
|
-
const sinon = require('sinon');
|
4
|
-
const crypto = require('crypto');
|
5
|
-
|
6
|
-
const readConfig = require('../read-config');
|
7
|
-
const dbutils = require('../db/utils');
|
8
|
-
|
9
|
-
let apiController;
|
10
|
-
|
11
|
-
const CLIENT_ID = '75edb050796a0eb1cf2cfb0da7245f85bc50baa7';
|
12
|
-
const PROVIDER = 'accounts.google.com';
|
13
|
-
const OPTIONS = {
|
14
|
-
externalUrl: 'https://my-cool-app.com',
|
15
|
-
samlAudience: 'https://saml.boxyhq.com',
|
16
|
-
samlPath: '/sso/oauth/saml',
|
17
|
-
db: {
|
18
|
-
engine: 'mem',
|
19
|
-
},
|
20
|
-
};
|
21
|
-
|
22
|
-
tap.before(async () => {
|
23
|
-
const controller = await require('../index.js')(OPTIONS);
|
24
|
-
|
25
|
-
apiController = controller.apiController;
|
26
|
-
});
|
27
|
-
|
28
|
-
tap.teardown(async () => {
|
29
|
-
process.exit(0);
|
30
|
-
});
|
31
|
-
|
32
|
-
tap.test('controller/api', async (t) => {
|
33
|
-
const metadataPath = path.join(__dirname, '/data/metadata');
|
34
|
-
const config = await readConfig(metadataPath);
|
35
|
-
|
36
|
-
t.test('.config()', async (t) => {
|
37
|
-
t.test('when required fields are missing or invalid', async (t) => {
|
38
|
-
t.test('when `rawMetadata` is empty', async (t) => {
|
39
|
-
const body = Object.assign({}, config[0]);
|
40
|
-
delete body['rawMetadata'];
|
41
|
-
|
42
|
-
try {
|
43
|
-
await apiController.config(body);
|
44
|
-
t.fail('Expecting JacksonError.');
|
45
|
-
} catch (err) {
|
46
|
-
t.equal(err.message, 'Please provide rawMetadata');
|
47
|
-
t.equal(err.statusCode, 400);
|
48
|
-
}
|
49
|
-
|
50
|
-
t.end();
|
51
|
-
});
|
52
|
-
|
53
|
-
t.test('when `defaultRedirectUrl` is empty', async (t) => {
|
54
|
-
const body = Object.assign({}, config[0]);
|
55
|
-
delete body['defaultRedirectUrl'];
|
56
|
-
|
57
|
-
try {
|
58
|
-
await apiController.config(body);
|
59
|
-
t.fail('Expecting JacksonError.');
|
60
|
-
} catch (err) {
|
61
|
-
t.equal(err.message, 'Please provide a defaultRedirectUrl');
|
62
|
-
t.equal(err.statusCode, 400);
|
63
|
-
}
|
64
|
-
|
65
|
-
t.end();
|
66
|
-
});
|
67
|
-
|
68
|
-
t.test('when `redirectUrl` is empty', async (t) => {
|
69
|
-
const body = Object.assign({}, config[0]);
|
70
|
-
delete body['redirectUrl'];
|
71
|
-
|
72
|
-
try {
|
73
|
-
await apiController.config(body);
|
74
|
-
t.fail('Expecting JacksonError.');
|
75
|
-
} catch (err) {
|
76
|
-
t.equal(err.message, 'Please provide redirectUrl');
|
77
|
-
t.equal(err.statusCode, 400);
|
78
|
-
}
|
79
|
-
|
80
|
-
t.end();
|
81
|
-
});
|
82
|
-
|
83
|
-
t.test('when `tenant` is empty', async (t) => {
|
84
|
-
const body = Object.assign({}, config[0]);
|
85
|
-
delete body['tenant'];
|
86
|
-
|
87
|
-
try {
|
88
|
-
await apiController.config(body);
|
89
|
-
t.fail('Expecting JacksonError.');
|
90
|
-
} catch (err) {
|
91
|
-
t.equal(err.message, 'Please provide tenant');
|
92
|
-
t.equal(err.statusCode, 400);
|
93
|
-
}
|
94
|
-
|
95
|
-
t.end();
|
96
|
-
});
|
97
|
-
|
98
|
-
t.test('when `product` is empty', async (t) => {
|
99
|
-
const body = Object.assign({}, config[0]);
|
100
|
-
delete body['product'];
|
101
|
-
|
102
|
-
try {
|
103
|
-
await apiController.config(body);
|
104
|
-
t.fail('Expecting JacksonError.');
|
105
|
-
} catch (err) {
|
106
|
-
t.equal(err.message, 'Please provide product');
|
107
|
-
t.equal(err.statusCode, 400);
|
108
|
-
}
|
109
|
-
|
110
|
-
t.end();
|
111
|
-
});
|
112
|
-
|
113
|
-
t.test('when `rawMetadata` is not a valid XML', async (t) => {
|
114
|
-
const body = Object.assign({}, config[0]);
|
115
|
-
body['rawMetadata'] = 'not a valid XML';
|
116
|
-
|
117
|
-
try {
|
118
|
-
await apiController.config(body);
|
119
|
-
t.fail('Expecting Error.');
|
120
|
-
} catch (err) {
|
121
|
-
t.match(err.message, /Non-whitespace before first tag./);
|
122
|
-
}
|
123
|
-
|
124
|
-
t.end();
|
125
|
-
});
|
126
|
-
});
|
127
|
-
|
128
|
-
t.test('when the request is good', async (t) => {
|
129
|
-
const body = Object.assign({}, config[0]);
|
130
|
-
|
131
|
-
const kdStub = sinon.stub(dbutils, 'keyDigest').returns(CLIENT_ID);
|
132
|
-
const rbStub = sinon
|
133
|
-
.stub(crypto, 'randomBytes')
|
134
|
-
.returns('f3b0f91eb8f4a9f7cc2254e08682d50b05b5d36262929e7f');
|
135
|
-
|
136
|
-
const response = await apiController.config(body);
|
137
|
-
t.ok(kdStub.called);
|
138
|
-
t.ok(rbStub.calledOnce);
|
139
|
-
t.equal(response.client_id, CLIENT_ID);
|
140
|
-
t.equal(
|
141
|
-
response.client_secret,
|
142
|
-
'f3b0f91eb8f4a9f7cc2254e08682d50b05b5d36262929e7f'
|
143
|
-
);
|
144
|
-
t.equal(response.provider, PROVIDER);
|
145
|
-
|
146
|
-
let savedConf = await apiController.getConfig({
|
147
|
-
clientID: CLIENT_ID,
|
148
|
-
});
|
149
|
-
t.equal(savedConf.provider, PROVIDER);
|
150
|
-
try {
|
151
|
-
await apiController.deleteConfig({ clientID: CLIENT_ID });
|
152
|
-
t.fail('Expecting JacksonError.');
|
153
|
-
} catch (err) {
|
154
|
-
t.equal(err.message, 'Please provide clientSecret');
|
155
|
-
t.equal(err.statusCode, 400);
|
156
|
-
}
|
157
|
-
try {
|
158
|
-
await apiController.deleteConfig({
|
159
|
-
clientID: CLIENT_ID,
|
160
|
-
clientSecret: 'xxxxx',
|
161
|
-
});
|
162
|
-
t.fail('Expecting JacksonError.');
|
163
|
-
} catch (err) {
|
164
|
-
t.equal(err.message, 'clientSecret mismatch');
|
165
|
-
t.equal(err.statusCode, 400);
|
166
|
-
}
|
167
|
-
await apiController.deleteConfig({
|
168
|
-
clientID: CLIENT_ID,
|
169
|
-
clientSecret: 'f3b0f91eb8f4a9f7cc2254e08682d50b05b5d36262929e7f',
|
170
|
-
});
|
171
|
-
savedConf = await apiController.getConfig({
|
172
|
-
clientID: CLIENT_ID,
|
173
|
-
});
|
174
|
-
t.same(savedConf, {}, 'should return empty config');
|
175
|
-
|
176
|
-
dbutils.keyDigest.restore();
|
177
|
-
crypto.randomBytes.restore();
|
178
|
-
|
179
|
-
t.end();
|
180
|
-
});
|
181
|
-
|
182
|
-
t.end();
|
183
|
-
});
|
184
|
-
|
185
|
-
t.end();
|
186
|
-
});
|