diff --git a/CHANGES b/CHANGES index be8e347de..ab3d4f46e 100644 --- a/CHANGES +++ b/CHANGES @@ -2158,11 +2158,11 @@ [6.1.0] * mail: update haraka to 2.8.26. this fixes zero-length queue file crash * update: set/unset appStoreId from the update route -* authProxy: Do not follow redirects +* proxyauth: Do not follow redirects +* proxyauth: add 2FA * appstore: add category translations * appstore: add media category * prepend the version to assets when sourcing to avoid cache hits on update * filemanger: list volumes of the app * Display upload size and size progress * nfs: chown the backups for hardlinks to work - diff --git a/src/proxyauth.js b/src/proxyauth.js index 27911c432..98b205d1f 100644 --- a/src/proxyauth.js +++ b/src/proxyauth.js @@ -24,6 +24,7 @@ const apps = require('./apps.js'), path = require('path'), paths = require('./paths.js'), safe = require('safetydance'), + speakeasy = require('speakeasy'), translation = require('./translation.js'), users = require('./users.js'); @@ -115,7 +116,7 @@ function auth(req, res, next) { } // endpoint called by login page, username and password posted as JSON body -function authenticate(req, res, next) { +function passwordAuth(req, res, next) { assert.strictEqual(typeof req.body, 'object'); const appId = req.headers['x-app-id'] || ''; @@ -123,14 +124,22 @@ function authenticate(req, res, next) { if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be non empty string' )); if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be non empty string' )); + if ('totpToken' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'totpToken must be a string' )); - const { username, password } = req.body; + const { username, password, totpToken } = req.body.username; const api = username.indexOf('@') !== -1 ? users.verifyWithEmail : users.verifyWithUsername; api(username, password, appId, function (error, user) { if (error) return next(new HttpError(403, 'Invalid username or password' )); + if (!user.ghost && !user.appPassword && user.twoFactorAuthenticationEnabled) { + if (!totpToken) return next(new HttpError(403, 'A totpToken must be provided')); + + let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 }); + if (!verified) return next(new HttpError(403, 'Invalid totpToken')); + } + req.user = user; next(); }); @@ -203,7 +212,7 @@ function initializeAuthwallExpressSync() { router.get ('/login', loginPage); router.get ('/auth', jwtVerify, basicAuthVerify, auth); - router.post('/login', json, authenticate, authorize); + router.post('/login', json, passwordAuth, authorize); router.get ('/logout', logoutPage); router.post('/logout', json, logout); diff --git a/src/routes/accesscontrol.js b/src/routes/accesscontrol.js index c6907f725..38a310803 100644 --- a/src/routes/accesscontrol.js +++ b/src/routes/accesscontrol.js @@ -21,17 +21,17 @@ function passwordAuth(req, res, next) { if (!req.body.username || typeof req.body.username !== 'string') return next(new HttpError(400, 'A username must be non-empty string')); if (!req.body.password || typeof req.body.password !== 'string') return next(new HttpError(400, 'A password must be non-empty string')); + if ('totpToken' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'totpToken must be a string' )); - const username = req.body.username; - const password = req.body.password; + const { username, password, totpToken } = req.body.username; function check2FA(user) { assert.strictEqual(typeof user, 'object'); if (!user.ghost && !user.appPassword && user.twoFactorAuthenticationEnabled) { - if (!req.body.totpToken) return next(new HttpError(401, 'A totpToken must be provided')); + if (!totpToken) return next(new HttpError(401, 'A totpToken must be provided')); - let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 }); + let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: totpToken, window: 2 }); if (!verified) return next(new HttpError(401, 'Invalid totpToken')); }