Skip to content

Commit a95bdba

Browse files
committed
ZMS-45: totp check, require session to allow checking totp
1 parent df2f69d commit a95bdba

2 files changed

Lines changed: 92 additions & 0 deletions

File tree

lib/api/2fa/totp.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,15 @@ module.exports = (db, server, userHandler) => {
256256
});
257257
}
258258

259+
// TOTP validation is only allowed for the currently authenticated user's session
260+
if (!req.accessToken || req.accessToken.user !== result.value.user) {
261+
res.status(403);
262+
return res.json({
263+
error: 'Invalid or missing session',
264+
code: 'InvalidToken'
265+
});
266+
}
267+
259268
// permissions check
260269
if (req.user && req.user === result.value.user) {
261270
req.validate(roles.can(req.role).readOwn('users'));

test/api/users-test.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
const supertest = require('supertest');
66
const chai = require('chai');
7+
const speakeasy = require('speakeasy');
78

89
const expect = chai.expect;
910
chai.config.includeStack = true;
@@ -301,6 +302,88 @@ describe('API Users', function () {
301302
await server.get(`/users/me?accessToken=${token2}`).expect(403);
302303
});
303304

305+
it('should POST /users/{user}/2fa/totp/check expect failure without a matching session', async () => {
306+
const setupResponse = await server
307+
.post(`/users/${user2}/2fa/totp/setup`)
308+
.send({
309+
issuer: 'WildDuck'
310+
})
311+
.expect(200);
312+
313+
expect(setupResponse.body.success).to.be.true;
314+
expect(setupResponse.body.seed).to.exist;
315+
316+
const enableToken = speakeasy.totp({
317+
secret: setupResponse.body.seed,
318+
encoding: 'base32'
319+
});
320+
321+
const enableResponse = await server
322+
.post(`/users/${user2}/2fa/totp/enable`)
323+
.send({
324+
token: enableToken
325+
})
326+
.expect(200);
327+
328+
expect(enableResponse.body.success).to.be.true;
329+
330+
const userTokenResponse = await server
331+
.post('/authenticate')
332+
.send({
333+
username: 'myuser2',
334+
password: 'secretvalue',
335+
token: true
336+
})
337+
.expect(200);
338+
339+
expect(userTokenResponse.body.success).to.be.true;
340+
expect(userTokenResponse.body.token).to.exist;
341+
342+
const user2TokenResponse = await server
343+
.post('/authenticate')
344+
.send({
345+
username: 'myuser2hash',
346+
password: 'test',
347+
token: true
348+
})
349+
.expect(200);
350+
351+
expect(user2TokenResponse.body.success).to.be.true;
352+
expect(user2TokenResponse.body.token).to.exist;
353+
354+
const totpToken = speakeasy.totp({
355+
secret: setupResponse.body.seed,
356+
encoding: 'base32'
357+
});
358+
359+
const noSessionResponse = await server
360+
.post(`/users/${user2}/2fa/totp/check`)
361+
.send({
362+
token: totpToken
363+
})
364+
.expect(403);
365+
366+
expect(noSessionResponse.body.code).to.equal('InvalidToken');
367+
368+
const wrongSessionResponse = await server
369+
.post(`/users/${user2}/2fa/totp/check?accessToken=${userTokenResponse.body.token}`)
370+
.send({
371+
token: totpToken
372+
})
373+
.expect(403);
374+
375+
expect(wrongSessionResponse.body.code).to.equal('InvalidToken');
376+
377+
const successResponse = await server
378+
.post(`/users/${user2}/2fa/totp/check?accessToken=${user2TokenResponse.body.token}`)
379+
.send({
380+
token: totpToken
381+
})
382+
.expect(200);
383+
384+
expect(successResponse.body.success).to.be.true;
385+
});
386+
304387
it('should PUT /users/{user}/logout expect success', async () => {
305388
// request logout
306389
const response = await server.put(`/users/${user}/logout`).send({ reason: 'Just because' }).expect(200);

0 commit comments

Comments
 (0)