@boxyhq/saml-jackson 0.1.6-beta.145 → 0.2.0
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/README.md +36 -9
- package/package.json +2 -1
- package/src/controller/error.js +12 -0
- package/src/controller/oauth/redirect.js +4 -3
- package/src/controller/oauth.js +48 -50
- package/src/jackson.js +35 -8
- package/src/test/data/metadata/boxyhq.js +6 -0
- package/src/test/data/metadata/boxyhq.xml +30 -0
- package/src/test/data/saml_response +1 -0
- package/src/test/oauth.test.js +342 -0
package/README.md
CHANGED
@@ -17,7 +17,7 @@ There are two ways to use this repo.
|
|
17
17
|
|
18
18
|
## Install as an npm library
|
19
19
|
|
20
|
-
Jackson is available as an [npm package](https://www.npmjs.com/package/@boxyhq/saml-jackson) that can be integrated into
|
20
|
+
Jackson is available as an [npm package](https://www.npmjs.com/package/@boxyhq/saml-jackson) that can be integrated into any web application framework (like Express.js for example). Please file an issue or submit a PR if you encounter any issues with your choice of framework.
|
21
21
|
|
22
22
|
```
|
23
23
|
npm i @boxyhq/saml-jackson
|
@@ -75,33 +75,60 @@ router.post('/api/v1/saml/config', async (req, res) => {
|
|
75
75
|
// OAuth 2.0 flow
|
76
76
|
router.get('/oauth/authorize', async (req, res) => {
|
77
77
|
try {
|
78
|
-
await oauthController.authorize(req
|
78
|
+
const { redirect_url } = await oauthController.authorize(req.query);
|
79
|
+
|
80
|
+
res.redirect(redirect_url);
|
79
81
|
} catch (err) {
|
80
|
-
|
82
|
+
const { message, statusCode = 500 } = err;
|
83
|
+
|
84
|
+
res.status(statusCode).send(message);
|
81
85
|
}
|
82
86
|
});
|
83
87
|
|
84
88
|
router.post('/oauth/saml', async (req, res) => {
|
85
89
|
try {
|
86
|
-
await oauthController.samlResponse(req
|
90
|
+
const { redirect_url } = await oauthController.samlResponse(req.body);
|
91
|
+
|
92
|
+
res.redirect(redirect_url);
|
87
93
|
} catch (err) {
|
88
|
-
|
94
|
+
const { message, statusCode = 500 } = err;
|
95
|
+
|
96
|
+
res.status(statusCode).send(message);
|
89
97
|
}
|
90
98
|
});
|
91
99
|
|
92
100
|
router.post('/oauth/token', cors(), async (req, res) => {
|
93
101
|
try {
|
94
|
-
await oauthController.token(req
|
102
|
+
const result = await oauthController.token(req.body);
|
103
|
+
|
104
|
+
res.json(result);
|
95
105
|
} catch (err) {
|
96
|
-
|
106
|
+
const { message, statusCode = 500 } = err;
|
107
|
+
|
108
|
+
res.status(statusCode).send(message);
|
97
109
|
}
|
98
110
|
});
|
99
111
|
|
100
112
|
router.get('/oauth/userinfo', async (req, res) => {
|
101
113
|
try {
|
102
|
-
|
114
|
+
let token = extractAuthToken(req);
|
115
|
+
|
116
|
+
// check for query param
|
117
|
+
if (!token) {
|
118
|
+
token = req.query.access_token;
|
119
|
+
}
|
120
|
+
|
121
|
+
if (!token) {
|
122
|
+
res.status(401).json({ message: 'Unauthorized' });
|
123
|
+
}
|
124
|
+
|
125
|
+
const profile = await oauthController.userInfo(token);
|
126
|
+
|
127
|
+
res.json(profile);
|
103
128
|
} catch (err) {
|
104
|
-
|
129
|
+
const { message, statusCode = 500 } = err;
|
130
|
+
|
131
|
+
res.status(statusCode).json({ message });
|
105
132
|
}
|
106
133
|
});
|
107
134
|
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@boxyhq/saml-jackson",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.2.0",
|
4
4
|
"license": "Apache 2.0",
|
5
5
|
"description": "SAML 2.0 service",
|
6
6
|
"main": "src/index.js",
|
@@ -57,6 +57,7 @@
|
|
57
57
|
"lint-staged": "12.1.2",
|
58
58
|
"nodemon": "2.0.15",
|
59
59
|
"prettier": "2.5.1",
|
60
|
+
"sinon": "12.0.1",
|
60
61
|
"tap": "15.1.5"
|
61
62
|
},
|
62
63
|
"lint-staged": {
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class JacksonError extends Error {
|
2
|
+
constructor(message, statusCode = 500) {
|
3
|
+
super(message);
|
4
|
+
|
5
|
+
this.name = this.constructor.name;
|
6
|
+
this.statusCode = statusCode;
|
7
|
+
|
8
|
+
Error.captureStackTrace(this, this.constructor);
|
9
|
+
}
|
10
|
+
}
|
11
|
+
|
12
|
+
module.exports = { JacksonError };
|
@@ -6,12 +6,13 @@ module.exports = {
|
|
6
6
|
res.redirect(url);
|
7
7
|
},
|
8
8
|
|
9
|
-
success: (
|
10
|
-
|
9
|
+
success: (redirectUrl, params) => {
|
10
|
+
const url = new URL(redirectUrl);
|
11
|
+
|
11
12
|
for (const [key, value] of Object.entries(params)) {
|
12
13
|
url.searchParams.set(key, value);
|
13
14
|
}
|
14
15
|
|
15
|
-
|
16
|
+
return url.href;
|
16
17
|
},
|
17
18
|
};
|
package/src/controller/oauth.js
CHANGED
@@ -2,10 +2,11 @@ const crypto = require('crypto');
|
|
2
2
|
|
3
3
|
const saml = require('../saml/saml.js');
|
4
4
|
const codeVerifier = require('./oauth/code-verifier.js');
|
5
|
-
const { indexNames
|
5
|
+
const { indexNames } = require('./utils.js');
|
6
6
|
const dbutils = require('../db/utils.js');
|
7
7
|
const redirect = require('./oauth/redirect.js');
|
8
8
|
const allowed = require('./oauth/allowed.js');
|
9
|
+
const { JacksonError } = require('./error.js');
|
9
10
|
|
10
11
|
let configStore;
|
11
12
|
let sessionStore;
|
@@ -33,7 +34,7 @@ function getEncodedClientId(client_id) {
|
|
33
34
|
}
|
34
35
|
}
|
35
36
|
|
36
|
-
const authorize = async (
|
37
|
+
const authorize = async (body) => {
|
37
38
|
const {
|
38
39
|
response_type = 'code',
|
39
40
|
client_id,
|
@@ -45,16 +46,17 @@ const authorize = async (req, res) => {
|
|
45
46
|
code_challenge_method = '',
|
46
47
|
// eslint-disable-next-line no-unused-vars
|
47
48
|
provider = 'saml',
|
48
|
-
} =
|
49
|
+
} = body;
|
49
50
|
|
50
51
|
if (!redirect_uri) {
|
51
|
-
|
52
|
+
throw new JacksonError('Please specify a redirect URL.', 400);
|
52
53
|
}
|
53
54
|
|
54
55
|
if (!state) {
|
55
|
-
|
56
|
-
.
|
57
|
-
|
56
|
+
throw new JacksonError(
|
57
|
+
'Please specify a state to safeguard against XSRF attacks.',
|
58
|
+
400
|
59
|
+
);
|
58
60
|
}
|
59
61
|
|
60
62
|
let samlConfig;
|
@@ -74,7 +76,7 @@ const authorize = async (req, res) => {
|
|
74
76
|
});
|
75
77
|
|
76
78
|
if (!samlConfigs || samlConfigs.length === 0) {
|
77
|
-
|
79
|
+
throw new JacksonError('SAML configuration not found.', 403);
|
78
80
|
}
|
79
81
|
|
80
82
|
// TODO: Support multiple matches
|
@@ -89,7 +91,7 @@ const authorize = async (req, res) => {
|
|
89
91
|
});
|
90
92
|
|
91
93
|
if (!samlConfigs || samlConfigs.length === 0) {
|
92
|
-
|
94
|
+
throw new JacksonError('SAML configuration not found.', 403);
|
93
95
|
}
|
94
96
|
|
95
97
|
// TODO: Support multiple matches
|
@@ -97,11 +99,11 @@ const authorize = async (req, res) => {
|
|
97
99
|
}
|
98
100
|
|
99
101
|
if (!samlConfig) {
|
100
|
-
|
102
|
+
throw new JacksonError('SAML configuration not found.', 403);
|
101
103
|
}
|
102
104
|
|
103
105
|
if (!allowed.redirect(redirect_uri, samlConfig.redirectUrl)) {
|
104
|
-
|
106
|
+
throw new JacksonError('Redirect URL is not allowed.', 403);
|
105
107
|
}
|
106
108
|
|
107
109
|
const samlReq = saml.request({
|
@@ -121,24 +123,26 @@ const authorize = async (req, res) => {
|
|
121
123
|
code_challenge_method,
|
122
124
|
});
|
123
125
|
|
124
|
-
|
126
|
+
const redirectUrl = redirect.success(samlConfig.idpMetadata.sso.redirectUrl, {
|
125
127
|
RelayState: relayStatePrefix + sessionId,
|
126
128
|
SAMLRequest: Buffer.from(samlReq.request).toString('base64'),
|
127
129
|
});
|
130
|
+
|
131
|
+
return { redirect_url: redirectUrl };
|
128
132
|
};
|
129
133
|
|
130
|
-
const samlResponse = async (
|
131
|
-
const { SAMLResponse } =
|
134
|
+
const samlResponse = async (body) => {
|
135
|
+
const { SAMLResponse } = body; // RelayState will contain the sessionId from earlier quasi-oauth flow
|
132
136
|
|
133
|
-
let RelayState =
|
137
|
+
let RelayState = body.RelayState || '';
|
134
138
|
|
135
139
|
if (!options.idpEnabled && !RelayState.startsWith(relayStatePrefix)) {
|
136
140
|
// IDP is disabled so block the request
|
137
|
-
|
138
|
-
|
139
|
-
.
|
140
|
-
|
141
|
-
|
141
|
+
|
142
|
+
throw new JacksonError(
|
143
|
+
'IdP (Identity Provider) flow has been disabled. Please head to your Service Provider to login.',
|
144
|
+
403
|
145
|
+
);
|
142
146
|
}
|
143
147
|
|
144
148
|
if (!RelayState.startsWith(relayStatePrefix)) {
|
@@ -157,7 +161,7 @@ const samlResponse = async (req, res) => {
|
|
157
161
|
});
|
158
162
|
|
159
163
|
if (!samlConfigs || samlConfigs.length === 0) {
|
160
|
-
|
164
|
+
throw new JacksonError('SAML configuration not found.', 403);
|
161
165
|
}
|
162
166
|
|
163
167
|
// TODO: Support multiple matches
|
@@ -168,10 +172,9 @@ const samlResponse = async (req, res) => {
|
|
168
172
|
if (RelayState !== '') {
|
169
173
|
session = await sessionStore.get(RelayState);
|
170
174
|
if (!session) {
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
'Unable to validate state from the origin request.'
|
175
|
+
throw new JacksonError(
|
176
|
+
'Unable to validate state from the origin request.',
|
177
|
+
403
|
175
178
|
);
|
176
179
|
}
|
177
180
|
}
|
@@ -207,7 +210,7 @@ const samlResponse = async (req, res) => {
|
|
207
210
|
session.redirect_uri &&
|
208
211
|
!allowed.redirect(session.redirect_uri, samlConfig.redirectUrl)
|
209
212
|
) {
|
210
|
-
|
213
|
+
throw new JacksonError('Redirect URL is not allowed.', 403);
|
211
214
|
}
|
212
215
|
|
213
216
|
let params = {
|
@@ -218,33 +221,34 @@ const samlResponse = async (req, res) => {
|
|
218
221
|
params.state = session.state;
|
219
222
|
}
|
220
223
|
|
221
|
-
|
222
|
-
res,
|
224
|
+
const redirectUrl = redirect.success(
|
223
225
|
(session && session.redirect_uri) || samlConfig.defaultRedirectUrl,
|
224
226
|
params
|
225
227
|
);
|
228
|
+
|
229
|
+
return { redirect_url: redirectUrl };
|
226
230
|
};
|
227
231
|
|
228
|
-
const token = async (
|
232
|
+
const token = async (body) => {
|
229
233
|
const {
|
230
234
|
client_id,
|
231
235
|
client_secret,
|
232
236
|
code_verifier,
|
233
237
|
code,
|
234
238
|
grant_type = 'authorization_code',
|
235
|
-
} =
|
239
|
+
} = body;
|
236
240
|
|
237
241
|
if (grant_type !== 'authorization_code') {
|
238
|
-
|
242
|
+
throw new JacksonError('Unsupported grant_type', 400);
|
239
243
|
}
|
240
244
|
|
241
245
|
if (!code) {
|
242
|
-
|
246
|
+
throw new JacksonError('Please specify code', 400);
|
243
247
|
}
|
244
248
|
|
245
249
|
const codeVal = await codeStore.get(code);
|
246
250
|
if (!codeVal || !codeVal.profile) {
|
247
|
-
|
251
|
+
throw new JacksonError('Invalid code', 403);
|
248
252
|
}
|
249
253
|
|
250
254
|
if (client_id && client_secret) {
|
@@ -256,7 +260,7 @@ const token = async (req, res) => {
|
|
256
260
|
client_id !== codeVal.clientID ||
|
257
261
|
client_secret !== codeVal.clientSecret
|
258
262
|
) {
|
259
|
-
|
263
|
+
throw new JacksonError('Invalid client_id or client_secret', 401);
|
260
264
|
}
|
261
265
|
}
|
262
266
|
} else if (code_verifier) {
|
@@ -267,12 +271,13 @@ const token = async (req, res) => {
|
|
267
271
|
}
|
268
272
|
|
269
273
|
if (codeVal.session.code_challenge !== cv) {
|
270
|
-
|
274
|
+
throw new JacksonError('Invalid code_verifier', 401);
|
271
275
|
}
|
272
276
|
} else if (codeVal && codeVal.session) {
|
273
|
-
|
274
|
-
|
275
|
-
|
277
|
+
throw new JacksonError(
|
278
|
+
'Please specify client_secret or code_verifier',
|
279
|
+
401
|
280
|
+
);
|
276
281
|
}
|
277
282
|
|
278
283
|
// store details against a token
|
@@ -280,24 +285,17 @@ const token = async (req, res) => {
|
|
280
285
|
|
281
286
|
await tokenStore.put(token, codeVal.profile);
|
282
287
|
|
283
|
-
|
288
|
+
return {
|
284
289
|
access_token: token,
|
285
290
|
token_type: 'bearer',
|
286
291
|
expires_in: options.db.ttl,
|
287
|
-
}
|
292
|
+
};
|
288
293
|
};
|
289
294
|
|
290
|
-
const userInfo = async (
|
291
|
-
|
292
|
-
|
293
|
-
// check for query param
|
294
|
-
if (!token) {
|
295
|
-
token = req.query.access_token;
|
296
|
-
}
|
297
|
-
|
298
|
-
const profile = await tokenStore.get(token);
|
295
|
+
const userInfo = async (token) => {
|
296
|
+
const { claims } = await tokenStore.get(token);
|
299
297
|
|
300
|
-
|
298
|
+
return claims;
|
301
299
|
};
|
302
300
|
|
303
301
|
module.exports = (opts) => {
|
package/src/jackson.js
CHANGED
@@ -17,33 +17,60 @@ app.use(express.urlencoded({ extended: true }));
|
|
17
17
|
|
18
18
|
app.get(oauthPath + '/authorize', async (req, res) => {
|
19
19
|
try {
|
20
|
-
await oauthController.authorize(req
|
20
|
+
const { redirect_url } = await oauthController.authorize(req.query);
|
21
|
+
|
22
|
+
res.redirect(redirect_url);
|
21
23
|
} catch (err) {
|
22
|
-
|
24
|
+
const { message, statusCode = 500 } = err;
|
25
|
+
|
26
|
+
res.status(statusCode).send(message);
|
23
27
|
}
|
24
28
|
});
|
25
29
|
|
26
30
|
app.post(env.samlPath, async (req, res) => {
|
27
31
|
try {
|
28
|
-
await oauthController.samlResponse(req
|
32
|
+
const { redirect_url } = await oauthController.samlResponse(req.body);
|
33
|
+
|
34
|
+
res.redirect(redirect_url);
|
29
35
|
} catch (err) {
|
30
|
-
|
36
|
+
const { message, statusCode = 500 } = err;
|
37
|
+
|
38
|
+
res.status(statusCode).send(message);
|
31
39
|
}
|
32
40
|
});
|
33
41
|
|
34
42
|
app.post(oauthPath + '/token', cors(), async (req, res) => {
|
35
43
|
try {
|
36
|
-
await oauthController.token(req
|
44
|
+
const result = await oauthController.token(req.body);
|
45
|
+
|
46
|
+
res.json(result);
|
37
47
|
} catch (err) {
|
38
|
-
|
48
|
+
const { message, statusCode = 500 } = err;
|
49
|
+
|
50
|
+
res.status(statusCode).send(message);
|
39
51
|
}
|
40
52
|
});
|
41
53
|
|
42
54
|
app.get(oauthPath + '/userinfo', async (req, res) => {
|
43
55
|
try {
|
44
|
-
|
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);
|
45
70
|
} catch (err) {
|
46
|
-
|
71
|
+
const { message, statusCode = 500 } = err;
|
72
|
+
|
73
|
+
res.status(statusCode).json({ message });
|
47
74
|
}
|
48
75
|
});
|
49
76
|
|
@@ -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
|
+
});
|