diff --git a/src/routes/profile.js b/src/routes/profile.js index 6d2d59ce8..4d6e6d667 100644 --- a/src/routes/profile.js +++ b/src/routes/profile.js @@ -3,7 +3,10 @@ exports = module.exports = { get: get, update: update, - changePassword: changePassword + changePassword: changePassword, + setTwoFactorAuthenticationSecret: setTwoFactorAuthenticationSecret, + enableTwoFactorAuthentication: enableTwoFactorAuthentication, + disableTwoFactorAuthentication: disableTwoFactorAuthentication }; var assert = require('assert'), @@ -65,3 +68,34 @@ function changePassword(req, res, next) { next(new HttpSuccess(204)); }); } + +function setTwoFactorAuthenticationSecret(req, res, next) { + assert.strictEqual(typeof req.params.userId, 'string'); + + user.setTwoFactorAuthenticationSecret(req.params.userId, function (error, result) { + if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, 'TwoFactor Authentication is enabled, disable first')); + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(201, { enabled: false, secret: result.secret, qrcode: result.qrcode })); + }); +} + +function enableTwoFactorAuthentication(req, res, next) { + assert.strictEqual(typeof req.params.userId, 'string'); + + if (!req.body.totpToken || typeof req.body.totpToken !== 'string') return next(new HttpError(400, 'totpToken must be a nonempty string')); + + user.enableTwoFactorAuthentication(req.params.userId, req.body.totpToken, function (error) { + if (error) return next(new HttpError(500, error)); + next(new HttpSuccess(202, {})); + }); +} + +function disableTwoFactorAuthentication(req, res, next) { + assert.strictEqual(typeof req.params.userId, 'string'); + + user.disableTwoFactorAuthentication(req.params.userId, function (error) { + if (error) return next(new HttpError(500, error)); + next(new HttpSuccess(202, {})); + }); +} diff --git a/src/server.js b/src/server.js index 9a5772b2c..5d3085b5c 100644 --- a/src/server.js +++ b/src/server.js @@ -130,6 +130,10 @@ function initializeExpressSync() { router.get ('/api/v1/profile', profileScope, routes.profile.get); router.post('/api/v1/profile', profileScope, routes.profile.update); router.post('/api/v1/profile/password', profileScope, routes.user.verifyPassword, routes.profile.changePassword); + router.post('/api/v1/profile/twofactorauthentication', profileScope, routes.profile.setTwoFactorAuthenticationSecret); + router.post('/api/v1/profile/twofactorauthentication/enable', profileScope, routes.profile.enableTwoFactorAuthentication); + router.post('/api/v1/profile/twofactorauthentication/disable', profileScope, routes.user.verifyPassword, routes.profile.disableTwoFactorAuthentication); + // user routes router.get ('/api/v1/users', usersScope, routes.user.requireAdmin, routes.user.list); diff --git a/src/user.js b/src/user.js index b3befc31c..0c54665c9 100644 --- a/src/user.js +++ b/src/user.js @@ -21,7 +21,10 @@ exports = module.exports = { createOwner: createOwner, getOwner: getOwner, sendInvite: sendInvite, - setGroups: setGroups + setGroups: setGroups, + setTwoFactorAuthenticationSecret: setTwoFactorAuthenticationSecret, + enableTwoFactorAuthentication: enableTwoFactorAuthentication, + disableTwoFactorAuthentication: disableTwoFactorAuthentication }; var assert = require('assert'), @@ -37,7 +40,9 @@ var assert = require('assert'), GroupError = groups.GroupError, hat = require('hat'), mailer = require('./mailer.js'), + qrcode = require('qrcode'), safe = require('safetydance'), + speakeasy = require('speakeasy'), tokendb = require('./tokendb.js'), userdb = require('./userdb.js'), util = require('util'), @@ -560,3 +565,59 @@ function sendInvite(userId, options, callback) { }); } +function setTwoFactorAuthenticationSecret(userId, callback) { + assert.strictEqual(typeof userId, 'string'); + assert.strictEqual(typeof callback, 'function'); + + userdb.get(userId, function (error, result) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND)); + if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); + + if (result.twoFactorAuthenticationEnabled) return callback(new UserError(UserError.ALREADY_EXISTS, 'TwoFactor Authentication is enabled, disable first')); + + var secret = speakeasy.generateSecret({ name: 'cloudron' }); + + userdb.update(userId, { twoFactorAuthenticationSecret: secret.base32, twoFactorAuthenticationEnabled: false }, function (error) { + if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); + + qrcode.toDataURL(secret.otpauth_url, function (error, dataUrl) { + if (error) console.error(error); + + callback(null, { secret: secret.base32, qrcode: dataUrl }); + }); + }); + }); +} + +function enableTwoFactorAuthentication(userId, totpToken, callback) { + assert.strictEqual(typeof userId, 'string'); + assert.strictEqual(typeof totpToken, 'string'); + assert.strictEqual(typeof callback, 'function'); + + userdb.get(userId, function (error, result) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND)); + if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); + + var verified = speakeasy.totp.verify({ secret: result.twoFactorAuthenticationSecret, encoding: 'base32', token: totpToken }); + if (!verified) return callback(new UserError(UserError.BAD_TOKEN, 'Invalid token')); + + if (result.twoFactorAuthenticationEnabled) return callback(new UserError(UserError.ALREADY_EXISTS, 'TwoFactor Authentication is already enabled')); + + userdb.update(userId, { twoFactorAuthenticationEnabled: true }, function (error) { + if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); + + callback(null); + }); + }); +} + +function disableTwoFactorAuthentication(userId, callback) { + assert.strictEqual(typeof userId, 'string'); + assert.strictEqual(typeof callback, 'function'); + + userdb.update(userId, { twoFactorAuthenticationEnabled: false, twoFactorAuthenticationSecret: '' }, function (error) { + if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); + + callback(null); + }); +}