@boxyhq/saml-jackson 0.1.5 → 0.2.0-beta.149

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/src/jackson.js CHANGED
@@ -2,6 +2,7 @@ const express = require('express');
2
2
  const cors = require('cors');
3
3
 
4
4
  const env = require('./env.js');
5
+ const { extractAuthToken } = require('./controller/utils.js');
5
6
 
6
7
  let apiController;
7
8
  let oauthController;
@@ -16,33 +17,60 @@ app.use(express.urlencoded({ extended: true }));
16
17
 
17
18
  app.get(oauthPath + '/authorize', async (req, res) => {
18
19
  try {
19
- await oauthController.authorize(req, res);
20
+ const { redirect_url } = await oauthController.authorize(req.query);
21
+
22
+ res.redirect(redirect_url);
20
23
  } catch (err) {
21
- res.status(500).send(err.message);
24
+ const { message, statusCode = 500 } = err;
25
+
26
+ res.status(statusCode).send(message);
22
27
  }
23
28
  });
24
29
 
25
30
  app.post(env.samlPath, async (req, res) => {
26
31
  try {
27
- await oauthController.samlResponse(req, res);
32
+ const { redirect_url } = await oauthController.samlResponse(req.body);
33
+
34
+ res.redirect(redirect_url);
28
35
  } catch (err) {
29
- res.status(500).send(err.message);
36
+ const { message, statusCode = 500 } = err;
37
+
38
+ res.status(statusCode).send(message);
30
39
  }
31
40
  });
32
41
 
33
42
  app.post(oauthPath + '/token', cors(), async (req, res) => {
34
43
  try {
35
- await oauthController.token(req, res);
44
+ const result = await oauthController.token(req.body);
45
+
46
+ res.json(result);
36
47
  } catch (err) {
37
- res.status(500).send(err.message);
48
+ const { message, statusCode = 500 } = err;
49
+
50
+ res.status(statusCode).send(message);
38
51
  }
39
52
  });
40
53
 
41
- app.get(oauthPath + '/userinfo', cors(), async (req, res) => {
54
+ app.get(oauthPath + '/userinfo', async (req, res) => {
42
55
  try {
43
- await oauthController.userInfo(req, res);
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);
44
70
  } catch (err) {
45
- res.status(500).send(err.message);
71
+ const { message, statusCode = 500 } = err;
72
+
73
+ res.status(statusCode).json({ message });
46
74
  }
47
75
  });
48
76
 
@@ -66,8 +94,18 @@ if (env.useInternalServer) {
66
94
  internalApp.use(express.urlencoded({ extended: true }));
67
95
  }
68
96
 
97
+ const validateApiKey = (token) => {
98
+ return env.apiKeys.includes(token);
99
+ };
100
+
69
101
  internalApp.post(apiPath + '/config', async (req, res) => {
70
102
  try {
103
+ const apiKey = extractAuthToken(req);
104
+ if (!validateApiKey(apiKey)) {
105
+ res.status(401).send('Unauthorized');
106
+ return;
107
+ }
108
+
71
109
  res.json(await apiController.config(req.body));
72
110
  } catch (err) {
73
111
  res.status(500).json({
@@ -76,6 +114,22 @@ internalApp.post(apiPath + '/config', async (req, res) => {
76
114
  }
77
115
  });
78
116
 
117
+ internalApp.post(apiPath + '/config/get', 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.body));
126
+ } catch (err) {
127
+ res.status(500).json({
128
+ error: err.message,
129
+ });
130
+ }
131
+ });
132
+
79
133
  let internalServer = server;
80
134
  if (env.useInternalServer) {
81
135
  internalServer = internalApp.listen(env.internalHostPort, async () => {
@@ -0,0 +1,40 @@
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 CHANGED
@@ -5,6 +5,7 @@ const thumbprint = require('thumbprint');
5
5
  const xmlbuilder = require('xmlbuilder');
6
6
  const crypto = require('crypto');
7
7
  const xmlcrypto = require('xml-crypto');
8
+ const claims = require('./claims');
8
9
 
9
10
  const idPrefix = '_';
10
11
  const authnXPath =
@@ -120,6 +121,19 @@ module.exports = {
120
121
  return;
121
122
  }
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
+
123
137
  resolve(profile);
124
138
  }
125
139
  );
@@ -141,12 +155,20 @@ module.exports = {
141
155
  let X509Certificate = null;
142
156
  let ssoPostUrl = null;
143
157
  let ssoRedirectUrl = null;
158
+ let loginType = 'idp';
144
159
 
145
- const ssoDes = rambda.pathOr(
146
- [],
160
+ let ssoDes = rambda.pathOr(
161
+ null,
147
162
  'EntityDescriptor.IDPSSODescriptor',
148
163
  res
149
164
  );
165
+ if (!ssoDes) {
166
+ ssoDes = rambda.pathOr([], 'EntityDescriptor.SPSSODescriptor', res);
167
+ if (!ssoDes) {
168
+ loginType = 'sp';
169
+ }
170
+ }
171
+
150
172
  for (const ssoDesRec of ssoDes) {
151
173
  const keyDes = ssoDesRec['KeyDescriptor'];
152
174
  for (const keyDesRec of keyDes) {
@@ -157,7 +179,10 @@ module.exports = {
157
179
  }
158
180
  }
159
181
 
160
- const ssoSvc = ssoDesRec['SingleSignOnService'] || [];
182
+ const ssoSvc =
183
+ ssoDesRec['SingleSignOnService'] ||
184
+ ssoDesRec['AssertionConsumerService'] ||
185
+ [];
161
186
  for (const ssoSvcRec of ssoSvc) {
162
187
  if (
163
188
  rambda.pathOr('', '$.Binding', ssoSvcRec).endsWith('HTTP-POST')
@@ -188,6 +213,7 @@ module.exports = {
188
213
  if (ssoRedirectUrl) {
189
214
  ret.sso.redirectUrl = ssoRedirectUrl;
190
215
  }
216
+ ret.loginType = loginType;
191
217
 
192
218
  resolve(ret);
193
219
  }
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ defaultRedirectUrl: 'http://localhost:3000/sso/oauth/completed',
3
+ redirectUrl: '["http://localhost:3000"]',
4
+ tenant: 'boxyhq.com',
5
+ product: 'crm',
6
+ };
@@ -0,0 +1,30 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://accounts.google.com/o/saml2" validUntil="2026-06-22T18:39:53.000Z">
3
+ <md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
4
+ <md:KeyDescriptor use="signing">
5
+ <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6
+ <ds:X509Data>
7
+ <ds:X509Certificate>MIIDdDCCAlygAwIBAgIGAXo6K+u/MA0GCSqGSIb3DQEBCwUAMHsxFDASBgNVBAoTC0dvb2dsZSBJ
8
+ bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MQ8wDQYDVQQDEwZHb29nbGUxGDAWBgNVBAsTD0dv
9
+ b2dsZSBGb3IgV29yazELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEwHhcNMjEwNjIz
10
+ MTgzOTUzWhcNMjYwNjIyMTgzOTUzWjB7MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEWMBQGA1UEBxMN
11
+ TW91bnRhaW4gVmlldzEPMA0GA1UEAxMGR29vZ2xlMRgwFgYDVQQLEw9Hb29nbGUgRm9yIFdvcmsx
12
+ CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
13
+ MIIBCgKCAQEA4qZcxwPiVka9GzGdQ9LVlgVkn3A7O3HtxR6RIm5AMaL4YZziEHt2HgxLdJZyXYJw
14
+ yfT1KB2IHt+XDQBkgEpQVXuuwSPI8vhI8Jr+nr8zia3MMoy9vJF8ZG7HuWeakaKEh7tJqjYu1Cl9
15
+ a81rkYdXAFUA+gl2q+stvK26xylAUwptCJSQo0NanWzCq+k5zvX0uLmh58+W5Yv11hDTtAoW+1dH
16
+ LWUTHXPfoZINPRy5NGKJ2Onq5/D5XJRimNnUa2iYi0Yv9txp1RRq4dpB9MaVttt3iKyDo4/+8fg/
17
+ bL8BLhguiOeqcP4DEIzMuExi3bZAOu2NC7k7Qf28nA81LzP9DQIDAQABMA0GCSqGSIb3DQEBCwUA
18
+ A4IBAQARBNB3+MfmKr5WXNXXE9YwUzUGmpfbqUPXh2y2dOAkj6TzoekAsOLWB0p8oyJ5d1bFlTsx
19
+ i1OY9RuFl0tc35Jbo+ae5GfUvJmbnYGi9z8sBL55HY6x3KQNmM/ehof7ttZwvB6nwuRxAiGYG497
20
+ 3tSzrqMQzEskcgX1mlCW0vks/ztCaayprDXcCUxWdP9FaiSZDEXV6PHhFZgGlRNvERsgaMDJgOsq
21
+ v6hLX10Q9CtOWzqu18PI4DcfoZ7exWcC29yWvwZzDTfHGaSG1DtUFLwiQmhVUbfd7/fmLV+/iOxV
22
+ zI0b5xSYZOJ7Kena7gd5zGVrc2ygKAFKiffiI5GLmLkv</ds:X509Certificate>
23
+ </ds:X509Data>
24
+ </ds:KeyInfo>
25
+ </md:KeyDescriptor>
26
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
27
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://accounts.google.com/o/saml2"/>
28
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://accounts.google.com/o/saml2"/>
29
+ </md:IDPSSODescriptor>
30
+ </md:EntityDescriptor>
@@ -0,0 +1 @@
1
+ PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzYW1sMnA6UmVzcG9uc2UgeG1sbnM6c2FtbDJwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIERlc3RpbmF0aW9uPSJodHRwczovLzdlYTItMTAzLTE1My0xMDQtMTYxLm5ncm9rLmlvL3Nzby9vYXV0aC9zYW1sIiBJRD0iXzRkZmM5MjYwZDFjZTVlMDRhZTQ4ZTg4ZWJkNGNlOTY3IiBJblJlc3BvbnNlVG89Il9kYWNkMTRhZGVmMmNiMDc1NGM5NiIgSXNzdWVJbnN0YW50PSIyMDIxLTEyLTA2VDE1OjIzOjA3LjM2MFoiIFZlcnNpb249IjIuMCI+DQogICA8c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5odHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vby9zYW1sMjwvc2FtbDI6SXNzdWVyPg0KICAgPGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogICAgICA8ZHM6U2lnbmVkSW5mbz4NCiAgICAgICAgIDxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIiAvPg0KICAgICAgICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiIC8+DQogICAgICAgICA8ZHM6UmVmZXJlbmNlIFVSST0iI180ZGZjOTI2MGQxY2U1ZTA0YWU0OGU4OGViZDRjZTk2NyI+DQogICAgICAgICAgICA8ZHM6VHJhbnNmb3Jtcz4NCiAgICAgICAgICAgICAgIDxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIgLz4NCiAgICAgICAgICAgICAgIDxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiIC8+DQogICAgICAgICAgICA8L2RzOlRyYW5zZm9ybXM+DQogICAgICAgICAgICA8ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2IiAvPg0KICAgICAgICAgICAgPGRzOkRpZ2VzdFZhbHVlPjI3Vy9EZTVKTlZIWkk1VTVKZGIvUi9mUXIya0pmd1VPbk0vVlRmQ1ZwQU09PC9kczpEaWdlc3RWYWx1ZT4NCiAgICAgICAgIDwvZHM6UmVmZXJlbmNlPg0KICAgICAgPC9kczpTaWduZWRJbmZvPg0KICAgICAgPGRzOlNpZ25hdHVyZVZhbHVlPm4vWCsvb0ZzK3JNU2gxMlg4dnowWlNkNlFpcU50eTY3RnFVbVNwdGllRmNoM0NScGQzZ2dpSHNwTTlMcm9MWjZVZGlsbThtdFZqTkgNCi9ob21yWDNvVEQ4UStxdzNiSllOTEptNEQvRWNmZmRDNmpSb0RJZzRYeFYxYlBVaWhQTnI4dlBFZEF2eEdwNTNiZ2MyREJsWkJpT3gNCnh4RlBSbEtnajJDWjh5SWk1R05FMUVTYms2SEtjY3g4R2dxWmtSYkVRbWtnbVMxZG1xcGl5bUpQM3orMHlaaXQyZ3dwQW5WVjNCMDUNCnZOcy8rSTFzRlZLaHNWcTc4QzZNWlZzV1pUSi80RFhadWhVSnpLNElTSU11b1RqUWFacEZMeEpBKzlhZzIvcm9OMjkwcitpcTZ5MVQNCjNHSlF1TlBPU0JVS1NpWlZVNmdwTldRRDBxckFxSWFQUFpOZnp3PT08L2RzOlNpZ25hdHVyZVZhbHVlPg0KICAgICAgPGRzOktleUluZm8+DQogICAgICAgICA8ZHM6WDUwOURhdGE+DQogICAgICAgICAgICA8ZHM6WDUwOVN1YmplY3ROYW1lPlNUPUNhbGlmb3JuaWEsQz1VUyxPVT1Hb29nbGUgRm9yIFdvcmssQ049R29vZ2xlLEw9TW91bnRhaW4gVmlldyxPPUdvb2dsZSBJbmMuPC9kczpYNTA5U3ViamVjdE5hbWU+DQogICAgICAgICAgICA8ZHM6WDUwOUNlcnRpZmljYXRlPk1JSURkRENDQWx5Z0F3SUJBZ0lHQVhvNksrdS9NQTBHQ1NxR1NJYjNEUUVCQ3dVQU1Ic3hGREFTQmdOVkJBb1RDMGR2YjJkc1pTQkoNCmJtTXVNUll3RkFZRFZRUUhFdzFOYjNWdWRHRnBiaUJXYVdWM01ROHdEUVlEVlFRREV3WkhiMjluYkdVeEdEQVdCZ05WQkFzVEQwZHYNCmIyZHNaU0JHYjNJZ1YyOXlhekVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXdIaGNOTWpFd05qSXoNCk1UZ3pPVFV6V2hjTk1qWXdOakl5TVRnek9UVXpXakI3TVJRd0VnWURWUVFLRXd0SGIyOW5iR1VnU1c1akxqRVdNQlFHQTFVRUJ4TU4NClRXOTFiblJoYVc0Z1ZtbGxkekVQTUEwR0ExVUVBeE1HUjI5dloyeGxNUmd3RmdZRFZRUUxFdzlIYjI5bmJHVWdSbTl5SUZkdmNtc3gNCkN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEENCk1JSUJDZ0tDQVFFQTRxWmN4d1BpVmthOUd6R2RROUxWbGdWa24zQTdPM0h0eFI2UkltNUFNYUw0WVp6aUVIdDJIZ3hMZEpaeVhZSncNCnlmVDFLQjJJSHQrWERRQmtnRXBRVlh1dXdTUEk4dmhJOEpyK25yOHppYTNNTW95OXZKRjhaRzdIdVdlYWthS0VoN3RKcWpZdTFDbDkNCmE4MXJrWWRYQUZVQStnbDJxK3N0dksyNnh5bEFVd3B0Q0pTUW8wTmFuV3pDcStrNXp2WDB1TG1oNTgrVzVZdjExaERUdEFvVysxZEgNCkxXVVRIWFBmb1pJTlBSeTVOR0tKMk9ucTUvRDVYSlJpbU5uVWEyaVlpMFl2OXR4cDFSUnE0ZHBCOU1hVnR0dDNpS3lEbzQvKzhmZy8NCmJMOEJMaGd1aU9lcWNQNERFSXpNdUV4aTNiWkFPdTJOQzdrN1FmMjhuQTgxTHpQOURRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkN3VUENCkE0SUJBUUFSQk5CMytNZm1LcjVXWE5YWEU5WXdVelVHbXBmYnFVUFhoMnkyZE9Ba2o2VHpvZWtBc09MV0IwcDhveUo1ZDFiRmxUc3gNCmkxT1k5UnVGbDB0YzM1SmJvK2FlNUdmVXZKbWJuWUdpOXo4c0JMNTVIWTZ4M0tRTm1NL2Vob2Y3dHRad3ZCNm53dVJ4QWlHWUc0OTcNCjN0U3pycU1RekVza2NnWDFtbENXMHZrcy96dENhYXlwckRYY0NVeFdkUDlGYWlTWkRFWFY2UEhoRlpnR2xSTnZFUnNnYU1ESmdPc3ENCnY2aExYMTBROUN0T1d6cXUxOFBJNERjZm9aN2V4V2NDMjl5V3Z3WnpEVGZIR2FTRzFEdFVGTHdpUW1oVlViZmQ3L2ZtTFYrL2lPeFYNCnpJMGI1eFNZWk9KN0tlbmE3Z2Q1ekdWcmMyeWdLQUZLaWZmaUk1R0xtTGt2PC9kczpYNTA5Q2VydGlmaWNhdGU+DQogICAgICAgICA8L2RzOlg1MDlEYXRhPg0KICAgICAgPC9kczpLZXlJbmZvPg0KICAgPC9kczpTaWduYXR1cmU+DQogICA8c2FtbDJwOlN0YXR1cz4NCiAgICAgIDxzYW1sMnA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIiAvPg0KICAgPC9zYW1sMnA6U3RhdHVzPg0KICAgPHNhbWwyOkFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9Il8xMWZkN2U5MDdjZmNiMTEwODM3NjI5OGM1Nzc0ZjgyNyIgSXNzdWVJbnN0YW50PSIyMDIxLTEyLTA2VDE1OjIzOjA3LjM2MFoiIFZlcnNpb249IjIuMCI+DQogICAgICA8c2FtbDI6SXNzdWVyPmh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL3NhbWwyPC9zYW1sMjpJc3N1ZXI+DQogICAgICA8c2FtbDI6U3ViamVjdD4NCiAgICAgICAgIDxzYW1sMjpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPmtpcmFuQGRlbW8uY29tPC9zYW1sMjpOYW1lSUQ+DQogICAgICAgICA8c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPg0KICAgICAgICAgICAgPHNhbWwyOlN1YmplY3RDb25maXJtYXRpb25EYXRhIEluUmVzcG9uc2VUbz0iX2RhY2QxNGFkZWYyY2IwNzU0Yzk2IiBOb3RPbk9yQWZ0ZXI9IjIwMjEtMTItMDZUMTU6Mjg6MDcuMzYwWiIgUmVjaXBpZW50PSJodHRwczovLzdlYTItMTAzLTE1My0xMDQtMTYxLm5ncm9rLmlvL3Nzby9vYXV0aC9zYW1sIiAvPg0KICAgICAgICAgPC9zYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgICAgPC9zYW1sMjpTdWJqZWN0Pg0KICAgICAgPHNhbWwyOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDIxLTEyLTA2VDE1OjE4OjA3LjM2MFoiIE5vdE9uT3JBZnRlcj0iMjAyMS0xMi0wNlQxNToyODowNy4zNjBaIj4NCiAgICAgICAgIDxzYW1sMjpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICAgICAgPHNhbWwyOkF1ZGllbmNlPmh0dHBzOi8vc2FtbC5ib3h5aHEuY29tPC9zYW1sMjpBdWRpZW5jZT4NCiAgICAgICAgIDwvc2FtbDI6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgIDwvc2FtbDI6Q29uZGl0aW9ucz4NCiAgICAgIDxzYW1sMjpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICAgICA8c2FtbDI6QXR0cmlidXRlIE5hbWU9InVzZXIuZW1haWwiPg0KICAgICAgICAgICAgPHNhbWwyOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOmFueVR5cGUiPmtpcmFuQGRlbW8uY29tPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgICAgIDwvc2FtbDI6QXR0cmlidXRlPg0KICAgICAgICAgPHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJ1c2VyLmZpcnN0TmFtZSI+DQogICAgICAgICAgICA8c2FtbDI6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6YW55VHlwZSI+S2lyYW48L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgICAgPC9zYW1sMjpBdHRyaWJ1dGU+DQogICAgICAgICA8c2FtbDI6QXR0cmlidXRlIE5hbWU9InVzZXIuaWQiIC8+DQogICAgICAgICA8c2FtbDI6QXR0cmlidXRlIE5hbWU9InVzZXIubGFzdE5hbWUiPg0KICAgICAgICAgICAgPHNhbWwyOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOmFueVR5cGUiPks8L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgICAgPC9zYW1sMjpBdHRyaWJ1dGU+DQogICAgICAgICA8c2FtbDI6QXR0cmlidXRlIE5hbWU9Im1lbWJlci1vZiIgLz4NCiAgICAgIDwvc2FtbDI6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWwyOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAyMS0xMi0wNlQxNToxNzowNS4wMDBaIiBTZXNzaW9uSW5kZXg9Il8xMWZkN2U5MDdjZmNiMTEwODM3NjI5OGM1Nzc0ZjgyNyI+DQogICAgICAgICA8c2FtbDI6QXV0aG5Db250ZXh0Pg0KICAgICAgICAgICAgPHNhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOnVuc3BlY2lmaWVkPC9zYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgICAgIDwvc2FtbDI6QXV0aG5Db250ZXh0Pg0KICAgICAgPC9zYW1sMjpBdXRoblN0YXRlbWVudD4NCiAgIDwvc2FtbDI6QXNzZXJ0aW9uPg0KPC9zYW1sMnA6UmVzcG9uc2U+
@@ -0,0 +1,342 @@
1
+ const tap = require('tap');
2
+ const { promises: fs } = require('fs');
3
+ const path = require('path');
4
+ const sinon = require('sinon');
5
+ const crypto = require('crypto');
6
+
7
+ const readConfig = require('../read-config');
8
+ const saml = require('../saml/saml');
9
+
10
+ let apiController;
11
+ let oauthController;
12
+
13
+ const code = '1234567890';
14
+ const token = '24c1550190dd6a5a9bd6fe2a8ff69d593121c7b9';
15
+
16
+ const metadataPath = path.join(__dirname, '/data/metadata');
17
+
18
+ const options = {
19
+ externalUrl: 'https://my-cool-app.com',
20
+ samlAudience: 'https://saml.boxyhq.com',
21
+ samlPath: '/sso/oauth/saml',
22
+ db: {
23
+ engine: 'mem',
24
+ },
25
+ };
26
+
27
+ const samlConfig = {
28
+ tenant: 'boxyhq.com',
29
+ product: 'crm',
30
+ redirectUrl: '["http://localhost:3000/*"]',
31
+ defaultRedirectUrl: 'http://localhost:3000/login/saml',
32
+ rawMetadata: null,
33
+ };
34
+
35
+ const addMetadata = async (metadataPath) => {
36
+ const configs = await readConfig(metadataPath);
37
+
38
+ for (const config of configs) {
39
+ await apiController.config(config);
40
+ }
41
+ };
42
+
43
+ tap.before(async () => {
44
+ const controller = await require('../index.js')(options);
45
+
46
+ apiController = controller.apiController;
47
+ oauthController = controller.oauthController;
48
+
49
+ await addMetadata(metadataPath);
50
+ });
51
+
52
+ tap.teardown(async () => {
53
+ process.exit(0);
54
+ });
55
+
56
+ tap.test('authorize()', async (t) => {
57
+ t.test('Should throw an error if `redirect_uri` null', async (t) => {
58
+ const body = {
59
+ redirect_uri: null,
60
+ state: 'state',
61
+ };
62
+
63
+ try {
64
+ await oauthController.authorize(body);
65
+ t.fail('Expecting JacksonError.');
66
+ } catch (err) {
67
+ t.equal(
68
+ err.message,
69
+ 'Please specify a redirect URL.',
70
+ 'got expected error message'
71
+ );
72
+ t.equal(err.statusCode, 400, 'got expected status code');
73
+ }
74
+
75
+ t.end();
76
+ });
77
+
78
+ t.test('Should throw an error if `state` null', async (t) => {
79
+ const body = {
80
+ redirect_uri: 'https://example.com/',
81
+ state: null,
82
+ };
83
+
84
+ try {
85
+ await oauthController.authorize(body);
86
+
87
+ t.fail('Expecting JacksonError.');
88
+ } catch (err) {
89
+ t.equal(
90
+ err.message,
91
+ 'Please specify a state to safeguard against XSRF attacks.',
92
+ 'got expected error message'
93
+ );
94
+ t.equal(err.statusCode, 400, 'got expected status code');
95
+ }
96
+
97
+ t.end();
98
+ });
99
+
100
+ t.test('Should throw an error if `client_id` is invalid', async (t) => {
101
+ const body = {
102
+ redirect_uri: 'https://example.com/',
103
+ state: 'state-123',
104
+ client_id: '27fa9a11875ec3a0',
105
+ };
106
+
107
+ try {
108
+ await oauthController.authorize(body);
109
+
110
+ t.fail('Expecting JacksonError.');
111
+ } catch (err) {
112
+ t.equal(
113
+ err.message,
114
+ 'SAML configuration not found.',
115
+ 'got expected error message'
116
+ );
117
+ t.equal(err.statusCode, 403, 'got expected status code');
118
+ }
119
+
120
+ t.end();
121
+ });
122
+
123
+ t.test(
124
+ 'Should throw an error if `redirect_uri` is not allowed',
125
+ async (t) => {
126
+ const body = {
127
+ redirect_uri: 'https://example.com/',
128
+ state: 'state-123',
129
+ client_id: `tenant=${samlConfig.tenant}&product=${samlConfig.product}`,
130
+ };
131
+
132
+ try {
133
+ await oauthController.authorize(body);
134
+
135
+ t.fail('Expecting JacksonError.');
136
+ } catch (err) {
137
+ t.equal(
138
+ err.message,
139
+ 'Redirect URL is not allowed.',
140
+ 'got expected error message'
141
+ );
142
+ t.equal(err.statusCode, 403, 'got expected status code');
143
+ }
144
+
145
+ t.end();
146
+ }
147
+ );
148
+
149
+ t.test('Should return the Idp SSO URL', async (t) => {
150
+ const body = {
151
+ redirect_uri: samlConfig.defaultRedirectUrl,
152
+ state: 'state-123',
153
+ client_id: `tenant=${samlConfig.tenant}&product=${samlConfig.product}`,
154
+ };
155
+
156
+ const response = await oauthController.authorize(body);
157
+ const params = new URLSearchParams(new URL(response.redirect_url).search);
158
+
159
+ t.ok('redirect_url' in response, 'got the Idp authorize URL');
160
+ t.ok(params.has('RelayState'), 'RelayState present in the query string');
161
+ t.ok(params.has('SAMLRequest'), 'SAMLRequest present in the query string');
162
+
163
+ t.end();
164
+ });
165
+
166
+ t.end();
167
+ });
168
+
169
+ tap.test('samlResponse()', async (t) => {
170
+ const authBody = {
171
+ redirect_uri: samlConfig.defaultRedirectUrl,
172
+ state: 'state-123',
173
+ client_id: `tenant=${samlConfig.tenant}&product=${samlConfig.product}`,
174
+ };
175
+
176
+ const { redirect_url } = await oauthController.authorize(authBody);
177
+
178
+ const relayState = new URLSearchParams(new URL(redirect_url).search).get(
179
+ 'RelayState'
180
+ );
181
+
182
+ const rawResponse = await fs.readFile(
183
+ path.join(__dirname, '/data/saml_response'),
184
+ 'utf8'
185
+ );
186
+
187
+ t.test('Should throw an error if `RelayState` is missing', async (t) => {
188
+ const responseBody = {
189
+ SAMLResponse: rawResponse,
190
+ };
191
+
192
+ try {
193
+ await oauthController.samlResponse(responseBody);
194
+
195
+ t.fail('Expecting JacksonError.');
196
+ } catch (err) {
197
+ t.equal(
198
+ err.message,
199
+ 'IdP (Identity Provider) flow has been disabled. Please head to your Service Provider to login.',
200
+ 'got expected error message'
201
+ );
202
+
203
+ t.equal(err.statusCode, 403, 'got expected status code');
204
+ }
205
+
206
+ t.end();
207
+ });
208
+
209
+ t.test(
210
+ 'Should return a URL with code and state as query params',
211
+ async (t) => {
212
+ const responseBody = {
213
+ SAMLResponse: rawResponse,
214
+ RelayState: relayState,
215
+ };
216
+
217
+ const stubValidateAsync = sinon.stub(saml, 'validateAsync').returns({
218
+ id: 1,
219
+ email: 'john@example.com',
220
+ firstName: 'John',
221
+ lastName: 'Doe',
222
+ });
223
+
224
+ const stubRandomBytes = sinon.stub(crypto, 'randomBytes').returns(code);
225
+
226
+ const response = await oauthController.samlResponse(responseBody);
227
+
228
+ const params = new URLSearchParams(new URL(response.redirect_url).search);
229
+
230
+ t.ok(stubValidateAsync.calledOnce, 'randomBytes called once');
231
+ t.ok(stubRandomBytes.calledOnce, 'validateAsync called once');
232
+ t.ok('redirect_url' in response, 'response contains redirect_url');
233
+ t.ok(params.has('code'), 'query string includes code');
234
+ t.ok(params.has('state'), 'query string includes state');
235
+ t.match(params.get('state'), authBody.state, 'state value is valid');
236
+
237
+ stubRandomBytes.restore();
238
+ stubValidateAsync.restore();
239
+
240
+ t.end();
241
+ }
242
+ );
243
+
244
+ t.end();
245
+ });
246
+
247
+ tap.test('token()', (t) => {
248
+ t.test(
249
+ 'Should throw an error if `grant_type` is not `authorization_code`',
250
+ async (t) => {
251
+ const body = {
252
+ grant_type: 'authorization_code_1',
253
+ };
254
+
255
+ try {
256
+ await oauthController.token(body);
257
+
258
+ t.fail('Expecting JacksonError.');
259
+ } catch (err) {
260
+ t.equal(
261
+ err.message,
262
+ 'Unsupported grant_type',
263
+ 'got expected error message'
264
+ );
265
+ t.equal(err.statusCode, 400, 'got expected status code');
266
+ }
267
+
268
+ t.end();
269
+ }
270
+ );
271
+
272
+ t.test('Should throw an error if `code` is missing', async (t) => {
273
+ const body = {
274
+ grant_type: 'authorization_code',
275
+ };
276
+
277
+ try {
278
+ await oauthController.token(body);
279
+
280
+ t.fail('Expecting JacksonError.');
281
+ } catch (err) {
282
+ t.equal(err.message, 'Please specify code', 'got expected error message');
283
+ t.equal(err.statusCode, 400, 'got expected status code');
284
+ }
285
+
286
+ t.end();
287
+ });
288
+
289
+ t.test('Should throw an error if `code` is invalid', async (t) => {
290
+ const body = {
291
+ grant_type: 'authorization_code',
292
+ client_id: `tenant=${samlConfig.tenant}&product=${samlConfig.product}`,
293
+ client_secret: 'some-secret',
294
+ redirect_uri: null,
295
+ code: 'invalid-code',
296
+ };
297
+
298
+ try {
299
+ await oauthController.token(body);
300
+
301
+ t.fail('Expecting JacksonError.');
302
+ } catch (err) {
303
+ t.equal(err.message, 'Invalid code', 'got expected error message');
304
+ t.equal(err.statusCode, 403, 'got expected status code');
305
+ }
306
+
307
+ t.end();
308
+ });
309
+
310
+ t.test('Should return the `access_token` for a valid request', async (t) => {
311
+ const body = {
312
+ grant_type: 'authorization_code',
313
+ client_id: `tenant=${samlConfig.tenant}&product=${samlConfig.product}`,
314
+ client_secret: 'some-secret',
315
+ redirect_uri: null,
316
+ code: code,
317
+ };
318
+
319
+ const stubRandomBytes = sinon.stub(crypto, 'randomBytes').returns(token);
320
+
321
+ const response = await oauthController.token(body);
322
+
323
+ t.ok(stubRandomBytes.calledOnce, 'randomBytes called once');
324
+ t.ok('access_token' in response, 'includes access_token');
325
+ t.ok('token_type' in response, 'includes token_type');
326
+ t.ok('expires_in' in response, 'includes expires_in');
327
+ t.match(response.access_token, token);
328
+ t.match(response.token_type, 'bearer');
329
+ t.match(response.expires_in, 300);
330
+
331
+ stubRandomBytes.reset();
332
+
333
+ t.end();
334
+ });
335
+
336
+ // TODO
337
+ t.test('Handle invalid client_id', async (t) => {
338
+ t.end();
339
+ });
340
+
341
+ t.end();
342
+ });