diff --git a/src/directoryserver.js b/src/directoryserver.js index 92f2e1a87..895b68926 100644 --- a/src/directoryserver.js +++ b/src/directoryserver.js @@ -22,7 +22,6 @@ const assert = require('assert'), reverseProxy = require('./reverseproxy.js'), safe = require('safetydance'), settings = require('./settings.js'), - speakeasy = require('speakeasy'), shell = require('./shell.js'), users = require('./users.js'), util = require('util'), @@ -279,17 +278,11 @@ async function userAuth(req, res, next) { verifyFunc = users.verifyWithUsername; } - const [error, user] = await safe(verifyFunc(commonName, req.credentials || '', '')); + const [error, user] = await safe(verifyFunc(commonName, req.credentials || '', '', { relaxedTotpCheck: true, totpToken })); if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.message)); - // currently this is only optional if totpToken is provided and user has 2fa enabled - if (totpToken && user.twoFactorAuthenticationEnabled) { - const verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: totpToken, window: 2 }); - if (!verified) return next(new ldap.InvalidCredentialsError(req.dn.toString())); - } - req.user = user; next(); diff --git a/src/externalldap.js b/src/externalldap.js index e16f64453..327ffe306 100644 --- a/src/externalldap.js +++ b/src/externalldap.js @@ -2,7 +2,6 @@ exports = module.exports = { verifyPassword, - verifyPasswordAndTotpToken, maybeCreateUser, testConfig, @@ -251,59 +250,33 @@ async function maybeCreateUser(identifier) { const user = translateUser(externalLdapConfig, ldapUsers[0]); if (!validUserRequirements(user)) throw new BoxError(BoxError.BAD_FIELD); - const [error, userId] = await safe(users.add(user.email, { username: user.username, password: null, displayName: user.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP_AUTO_CREATE)); - if (error) { - debug(`maybeCreateUser: failed to auto create user ${user.username}`, error); - throw error; + return await users.add(user.email, { username: user.username, password: null, displayName: user.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP_AUTO_CREATE); +} + +async function verifyPassword(user, password, totpToken) { + assert.strictEqual(typeof user, 'object'); + assert.strictEqual(typeof password, 'string'); + assert(totpToken === null || typeof totpToken === 'string'); + + const externalLdapConfig = await settings.getExternalLdapConfig(); + if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled'); + + const ldapUsers = await ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }); + if (ldapUsers.length === 0) throw new BoxError(BoxError.NOT_FOUND); + if (ldapUsers.length > 1) throw new BoxError(BoxError.CONFLICT); + + const client = await getClient(externalLdapConfig, { bind: false }); + + let userAuthDn; + if (totpToken) { + // inject totptoken into first attribute + const rdns = ldapUsers[0].dn.split(','); + userAuthDn = `${rdns[0]}+totptoken=${totpToken},` + rdns.slice(1).join(','); + } else { + userAuthDn = ldapUsers[0].dn; } - // fetch the full record and amend potential twoFA settings - const newUser = await users.get(userId); - if (user.twoFactorAuthenticationEnabled) newUser.twoFactorAuthenticationEnabled = true; - - return newUser; -} - -async function verifyPassword(user, password) { - assert.strictEqual(typeof user, 'object'); - assert.strictEqual(typeof password, 'string'); - - const externalLdapConfig = await settings.getExternalLdapConfig(); - if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled'); - - const ldapUsers = await ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }); - if (ldapUsers.length === 0) throw new BoxError(BoxError.NOT_FOUND); - if (ldapUsers.length > 1) throw new BoxError(BoxError.CONFLICT); - - const client = await getClient(externalLdapConfig, { bind: false }); - - const [error] = await safe(util.promisify(client.bind.bind(client))(ldapUsers[0].dn, password)); - client.unbind(); - if (error instanceof ldap.InvalidCredentialsError) throw new BoxError(BoxError.INVALID_CREDENTIALS); - if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error); - - return translateUser(externalLdapConfig, ldapUsers[0]); -} - -async function verifyPasswordAndTotpToken(user, password, totpToken) { - assert.strictEqual(typeof user, 'object'); - assert.strictEqual(typeof password, 'string'); - assert.strictEqual(typeof totpToken, 'string'); - - const externalLdapConfig = await settings.getExternalLdapConfig(); - if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled'); - - const ldapUsers = await ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }); - if (ldapUsers.length === 0) throw new BoxError(BoxError.NOT_FOUND); - if (ldapUsers.length > 1) throw new BoxError(BoxError.CONFLICT); - - const client = await getClient(externalLdapConfig, { bind: false }); - - // inject totptoken into first attribute - const rdns = ldapUsers[0].dn.split(','); - const totpTokenDn = `${rdns[0]}+totptoken=${totpToken},` + rdns.slice(1).join(','); - - const [error] = await safe(util.promisify(client.bind.bind(client))(totpTokenDn, password)); + const [error] = await safe(util.promisify(client.bind.bind(client))(userAuthDn, password)); client.unbind(); if (error instanceof ldap.InvalidCredentialsError) throw new BoxError(BoxError.INVALID_CREDENTIALS); if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error); diff --git a/src/ldap.js b/src/ldap.js index 279141b2d..953489ff2 100644 --- a/src/ldap.js +++ b/src/ldap.js @@ -52,6 +52,9 @@ async function userAuthInternal(appId, req, res, next) { const commonName = req.dn.rdns[0].attrs[attributeName].value; if (!commonName) return next(new ldap.NoSuchObjectError(req.dn.toString())); + const TOTPTOKEN_ATTRIBUTE_NAME = 'totptoken'; + const totpToken = req.dn.rdns[0].attrs[TOTPTOKEN_ATTRIBUTE_NAME] ? req.dn.rdns[0].attrs[TOTPTOKEN_ATTRIBUTE_NAME].value : null; + let verifyFunc; if (attributeName === 'mail') { verifyFunc = users.verifyWithEmail; @@ -63,7 +66,7 @@ async function userAuthInternal(appId, req, res, next) { verifyFunc = users.verifyWithUsername; } - const [error, user] = await safe(verifyFunc(commonName, req.credentials || '', appId || '')); + const [error, user] = await safe(verifyFunc(commonName, req.credentials || '', appId || '', { relaxedTotpCheck: true, totpToken })); if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.message)); @@ -465,13 +468,13 @@ async function verifyMailboxPassword(mailbox, password) { assert.strictEqual(typeof password, 'string'); if (mailbox.ownerType === mail.OWNERTYPE_USER) { - return await users.verify(mailbox.ownerId, password, users.AP_MAIL /* identifier */); + return await users.verify(mailbox.ownerId, password, users.AP_MAIL /* identifier */, { relaxedTotpCheck: true }); } else if (mailbox.ownerType === mail.OWNERTYPE_GROUP) { const userIds = await groups.getMembers(mailbox.ownerId); let verifiedUser = null; for (const userId of userIds) { - const [error, result] = await safe(users.verify(userId, password, users.AP_MAIL /* identifier */)); + const [error, result] = await safe(users.verify(userId, password, users.AP_MAIL /* identifier */, { relaxedTotpCheck: true })); if (error) continue; // try the next user verifiedUser = result; break; // found a matching validated user @@ -496,7 +499,7 @@ async function authenticateSftp(req, res, next) { let [error, app] = await safe(apps.getByFqdn(parts[1])); if (error || !app) return next(new ldap.InvalidCredentialsError(req.dn.toString())); - [error] = await safe(users.verifyWithUsername(parts[0], req.credentials, app.id)); + [error] = await safe(users.verifyWithUsername(parts[0], req.credentials, app.id, { relaxedTotpCheck: true })); if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString())); debug('sftp auth: success'); diff --git a/src/proxyauth.js b/src/proxyauth.js index adda41145..aec3a66ff 100644 --- a/src/proxyauth.js +++ b/src/proxyauth.js @@ -15,7 +15,6 @@ const apps = require('./apps.js'), debug = require('debug')('box:proxyAuth'), ejs = require('ejs'), express = require('express'), - externalLdap = require('./externalldap.js'), hat = require('./hat.js'), http = require('http'), HttpError = require('connect-lastmile').HttpError, @@ -26,7 +25,6 @@ const apps = require('./apps.js'), paths = require('./paths.js'), safe = require('safetydance'), settings = require('./settings.js'), - speakeasy = require('speakeasy'), translation = require('./translation.js'), users = require('./users.js'), util = require('util'); @@ -167,20 +165,8 @@ async function passwordAuth(req, res, next) { const { username, password, totpToken } = req.body; const verifyFunc = username.indexOf('@') !== -1 ? users.verifyWithEmail : users.verifyWithUsername; - const [error, user] = await safe(verifyFunc(username, password, appId)); - 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')); - - if (user.source === 'ldap') { - const [error] = await safe(externalLdap.verifyPasswordAndTotpToken(user, password, totpToken)); - if (error) return next(new HttpError(401, 'Invalid totpToken')); - } else { - const verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 }); - if (!verified) return next(new HttpError(401, 'Invalid totpToken')); - } - } + const [error, user] = await safe(verifyFunc(username, password, appId, { totpToken })); + if (error) return next(new HttpError(403, error.message)); req.user = user; next(); diff --git a/src/routes/accesscontrol.js b/src/routes/accesscontrol.js index 259225e74..b29b83775 100644 --- a/src/routes/accesscontrol.js +++ b/src/routes/accesscontrol.js @@ -12,10 +12,8 @@ const apps = require('../apps.js'), tokens = require('../tokens.js'), assert = require('assert'), BoxError = require('../boxerror.js'), - externalLdap = require('../externalldap.js'), HttpError = require('connect-lastmile').HttpError, safe = require('safetydance'), - speakeasy = require('speakeasy'), users = require('../users.js'); async function passwordAuth(req, res, next) { @@ -29,29 +27,12 @@ async function passwordAuth(req, res, next) { const verifyFunc = username.indexOf('@') === -1 ? users.verifyWithUsername : users.verifyWithEmail; - let [error, user] = await safe(verifyFunc(username, password, users.AP_WEBADMIN)); - if (error && error.reason === BoxError.NOT_FOUND) { - [error, user] = await safe(externalLdap.maybeCreateUser(username.toLowerCase())); - if (error) return next(new HttpError(401, 'Unauthorized')); - [error] = await safe(externalLdap.verifyPassword(user, password)); - if (error) return next(new HttpError(401, 'Unauthorized')); - } - if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized')); + let [error, user] = await safe(verifyFunc(username, password, users.AP_WEBADMIN, { totpToken })); + if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, error.message)); + if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized')); if (error) return next(new HttpError(500, error)); if (!user) return next(new HttpError(401, 'Unauthorized')); - if (!user.ghost && !user.appPassword && user.twoFactorAuthenticationEnabled) { - if (!totpToken) return next(new HttpError(401, 'A totpToken must be provided')); - - if (user.source === 'ldap') { - const [error] = await safe(externalLdap.verifyPasswordAndTotpToken(user, password, totpToken)); - if (error) return next(new HttpError(401, 'Invalid totpToken')); - } else { - const verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: totpToken, window: 2 }); - if (!verified) return next(new HttpError(401, 'Invalid totpToken')); - } - } - req.user = user; next(); diff --git a/src/routes/profile.js b/src/routes/profile.js index 6edee6346..822de4a7a 100644 --- a/src/routes/profile.js +++ b/src/routes/profile.js @@ -75,7 +75,7 @@ async function update(req, res, next) { if (data.fallbackEmail) { if (!req.body.password || typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be non empty string')); - const [verifyError] = await safe(users.verify(req.user.id, req.body.password, users.AP_WEBADMIN)); + const [verifyError] = await safe(users.verify(req.user.id, req.body.password, users.AP_WEBADMIN, { relaxedTotpCheck: true })); if (verifyError) return next(BoxError.toHttpError(verifyError)); req.body.password = ''; // this will prevent logs from displaying plain text password diff --git a/src/routes/test/users-test.js b/src/routes/test/users-test.js index 116f9dc8d..e6a93fec4 100644 --- a/src/routes/test/users-test.js +++ b/src/routes/test/users-test.js @@ -175,7 +175,7 @@ describe('Users API', function () { }); it('did set password of created user', async function () { - await users.verify(userWithPassword.id, userWithPassword.password, users.AP_WEBADMIN); + await users.verify(userWithPassword.id, userWithPassword.password, users.AP_WEBADMIN, {}); }); }); @@ -445,7 +445,7 @@ describe('Users API', function () { }); it('did change the user password', async function () { - await users.verify(user.id, 'bigenough', users.AP_WEBADMIN); + await users.verify(user.id, 'bigenough', users.AP_WEBADMIN, {}); }); }); diff --git a/src/routes/users.js b/src/routes/users.js index f869e83ac..099782363 100644 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -142,7 +142,7 @@ async function verifyPassword(req, res, next) { if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires user password')); - const [error] = await safe(users.verify(req.user.id, req.body.password, users.AP_WEBADMIN)); + const [error] = await safe(users.verify(req.user.id, req.body.password, users.AP_WEBADMIN, { relaxedTotpCheck: true })); if (error) return next(BoxError.toHttpError(error)); req.body.password = ''; // this will prevent logs from displaying plain text password diff --git a/src/test/apppasswords-test.js b/src/test/apppasswords-test.js index dfa0c3096..b2fa21a82 100644 --- a/src/test/apppasswords-test.js +++ b/src/test/apppasswords-test.js @@ -54,25 +54,25 @@ describe('App passwords', function () { }); it('can verify app password', async function () { - const result = await users.verify(admin.id, password, 'appid'); + const result = await users.verify(admin.id, password, 'appid', {}); expect(result).to.be.ok(); expect(result.appPassword).to.be(true); }); it('can verify non-app password', async function () { - const result = await users.verify(admin.id, admin.password, 'appid'); + const result = await users.verify(admin.id, admin.password, 'appid', {}); expect(result).to.be.ok(); expect(result.appPassword).to.be(undefined); }); it('cannot verify bad password', async function () { - const [error, result] = await safe(users.verify(admin.id, 'bad', 'appid')); + const [error, result] = await safe(users.verify(admin.id, 'bad', 'appid', {})); expect(result).to.not.be.ok(); expect(error.reason).to.be(BoxError.INVALID_CREDENTIALS); }); it('cannot verify password for another app', async function () { - const [error, result] = await safe(users.verify(admin.id, password, 'appid2')); + const [error, result] = await safe(users.verify(admin.id, password, 'appid2', {})); expect(result).to.not.be.ok(); expect(error.reason).to.be(BoxError.INVALID_CREDENTIALS); }); @@ -82,7 +82,7 @@ describe('App passwords', function () { }); it('cannot verify deleted app password', async function () { - const [error] = await safe(users.verify(admin.id, password, 'appid')); + const [error] = await safe(users.verify(admin.id, password, 'appid', {})); expect(error.reason).to.be(BoxError.INVALID_CREDENTIALS); }); diff --git a/src/test/users-test.js b/src/test/users-test.js index 20dac753e..0aeccd956 100644 --- a/src/test/users-test.js +++ b/src/test/users-test.js @@ -244,43 +244,43 @@ describe('User', function () { before(createOwner); it('fails due to non existing user', async function () { - const [error] = await safe(users.verify('somerandomid', 'somepassword', users.AP_WEBADMIN)); + const [error] = await safe(users.verify('somerandomid', 'somepassword', users.AP_WEBADMIN, {})); expect(error.reason).to.equal(BoxError.NOT_FOUND); }); it('fails due to empty password', async function () { - const [error] = await safe(users.verify(admin.id, '', users.AP_WEBADMIN)); + const [error] = await safe(users.verify(admin.id, '', users.AP_WEBADMIN, {})); expect(error.reason).to.equal(BoxError.INVALID_CREDENTIALS); }); it('fails due to wrong password', async function () { - const [error] = await safe(users.verify(admin.id, admin.password+'x', users.AP_WEBADMIN)); + const [error] = await safe(users.verify(admin.id, admin.password+'x', users.AP_WEBADMIN, {})); expect(error.reason).to.equal(BoxError.INVALID_CREDENTIALS); }); it('succeeds', async function () { - const result = await users.verify(admin.id, admin.password, users.AP_WEBADMIN); + const result = await users.verify(admin.id, admin.password, users.AP_WEBADMIN, {}); expect(result).to.be.ok(); expect(result.appPassword).to.not.be.ok(); expect(result.ghost).to.not.be.ok(); }); it('fails for ghost if not enabled', async function () { - const [error] = await safe(users.verify(admin.id, 'foobar', users.AP_WEBADMIN)); + const [error] = await safe(users.verify(admin.id, 'foobar', users.AP_WEBADMIN, {})); expect(error.reason).to.equal(BoxError.INVALID_CREDENTIALS); }); it('fails for ghost with wrong password', async function () { await users.setGhost(admin, 'testpassword', 0); - const [error] = await safe(users.verify(admin.id, 'foobar', users.AP_WEBADMIN)); + const [error] = await safe(users.verify(admin.id, 'foobar', users.AP_WEBADMIN, {})); expect(error.reason).to.equal(BoxError.INVALID_CREDENTIALS); }); it('succeeds for ghost', async function () { await users.setGhost(admin, 'testpassword', 0); - const result = await users.verify(admin.id, 'testpassword', users.AP_WEBADMIN); + const result = await users.verify(admin.id, 'testpassword', users.AP_WEBADMIN, {}); expect(result.id).to.equal(admin.id); expect(result.ghost).to.be(true); }); @@ -288,7 +288,7 @@ describe('User', function () { it('succeeds for normal user password when ghost file exists', async function () { await users.setGhost(admin, 'testpassword', 0); - const result = await users.verify(admin.id, admin.password, users.AP_WEBADMIN); + const result = await users.verify(admin.id, admin.password, users.AP_WEBADMIN, {}); expect(result.id).to.equal(admin.id); expect(result.ghost).to.not.be.ok(); }); @@ -298,41 +298,41 @@ describe('User', function () { before(createOwner); it('fails due to non existing username', async function () { - const [error] = await safe(users.verifyWithUsername('someusername', 'somepass', users.AP_WEBADMIN)); + const [error] = await safe(users.verifyWithUsername('someusername', 'somepass', users.AP_WEBADMIN, {})); expect(error.reason).to.equal(BoxError.NOT_FOUND); }); it('fails due to empty password', async function () { - const [error] = await safe(users.verifyWithUsername(admin.username, '', users.AP_WEBADMIN)); + const [error] = await safe(users.verifyWithUsername(admin.username, '', users.AP_WEBADMIN, {})); expect(error.reason).to.equal(BoxError.INVALID_CREDENTIALS); }); it('fails due to wrong password', async function () { - const [error] = await safe(users.verifyWithUsername(admin.username, 'somepass', users.AP_WEBADMIN)); + const [error] = await safe(users.verifyWithUsername(admin.username, 'somepass', users.AP_WEBADMIN, {})); expect(error.reason).to.equal(BoxError.INVALID_CREDENTIALS); }); it('succeeds', async function () { - const result = await users.verifyWithUsername(admin.username, admin.password, users.AP_WEBADMIN); + const result = await users.verifyWithUsername(admin.username, admin.password, users.AP_WEBADMIN, {}); expect(result.id).to.equal(admin.id); }); it('succeeds for different username case', async function () { - const result = await users.verifyWithUsername(admin.username.toUpperCase(), admin.password, users.AP_WEBADMIN); + const result = await users.verifyWithUsername(admin.username.toUpperCase(), admin.password, users.AP_WEBADMIN, {}); expect(result.id).to.equal(admin.id); }); it('fails for ghost with wrong password', async function () { await users.setGhost(admin, 'testpassword', 0); - const [error] = await safe(users.verifyWithUsername(admin.username, 'foobar', users.AP_WEBADMIN)); + const [error] = await safe(users.verifyWithUsername(admin.username, 'foobar', users.AP_WEBADMIN, {})); expect(error.reason).to.equal(BoxError.INVALID_CREDENTIALS); }); it('succeeds for ghost', async function () { await users.setGhost(admin, 'testpassword', 0); - const result = await users.verifyWithUsername(admin.username, 'testpassword', users.AP_WEBADMIN); + const result = await users.verifyWithUsername(admin.username, 'testpassword', users.AP_WEBADMIN, {}); expect(result.id).to.equal(admin.id); expect(result.ghost).to.be(true); }); @@ -342,41 +342,41 @@ describe('User', function () { before(createOwner); it('fails due to non existing user', async function () { - const [error] = await safe(users.verifyWithEmail('bad@email.com', admin.password, users.AP_WEBADMIN)); + const [error] = await safe(users.verifyWithEmail('bad@email.com', admin.password, users.AP_WEBADMIN, {})); expect(error.reason).to.equal(BoxError.NOT_FOUND); }); it('fails due to empty password', async function () { - const [error] = await safe(users.verifyWithEmail(admin.email, '', users.AP_WEBADMIN)); + const [error] = await safe(users.verifyWithEmail(admin.email, '', users.AP_WEBADMIN, {})); expect(error.reason).to.equal(BoxError.INVALID_CREDENTIALS); }); it('fails due to wrong password', async function () { - const [error] = await safe(users.verifyWithEmail(admin.email, 'badpassword', users.AP_WEBADMIN)); + const [error] = await safe(users.verifyWithEmail(admin.email, 'badpassword', users.AP_WEBADMIN, {})); expect(error.reason).to.equal(BoxError.INVALID_CREDENTIALS); }); it('succeeds', async function () { - const result = await users.verifyWithEmail(admin.email, admin.password, users.AP_WEBADMIN); + const result = await users.verifyWithEmail(admin.email, admin.password, users.AP_WEBADMIN, {}); expect(result.id).to.be(admin.id); }); it('succeeds for different email case', async function () { - const result = await users.verifyWithEmail(admin.email.toUpperCase(), admin.password, users.AP_WEBADMIN); + const result = await users.verifyWithEmail(admin.email.toUpperCase(), admin.password, users.AP_WEBADMIN, {}); expect(result.id).to.be(admin.id); }); it('fails for ghost with wrong password', async function () { await users.setGhost(admin, 'testpassword', 0); - const [error] = await safe(users.verifyWithEmail(admin.email, 'foobar', users.AP_WEBADMIN)); + const [error] = await safe(users.verifyWithEmail(admin.email, 'foobar', users.AP_WEBADMIN, {})); expect(error.reason).to.equal(BoxError.INVALID_CREDENTIALS); }); it('succeeds for ghost', async function () { await users.setGhost(admin, 'testpassword', 0); - const result = await users.verifyWithEmail(admin.email, 'testpassword', users.AP_WEBADMIN); + const result = await users.verifyWithEmail(admin.email, 'testpassword', users.AP_WEBADMIN, {}); expect(result.id).to.equal(admin.id); expect(result.ghost).to.equal(true); }); @@ -387,13 +387,13 @@ describe('User', function () { it('verify fails for inactive user', async function () { await users.update(admin, { active: false }, auditSource); - const [error] = await safe(users.verify(admin.id, admin.password, users.AP_WEBADMIN)); + const [error] = await safe(users.verify(admin.id, admin.password, users.AP_WEBADMIN, {})); expect(error.reason).to.equal(BoxError.NOT_FOUND); }); it('verify succeeds for inactive user', async function () { await users.update(admin, { active: true }, auditSource); - await users.verify(admin.id, admin.password, users.AP_WEBADMIN); + await users.verify(admin.id, admin.password, users.AP_WEBADMIN, {}); }); }); @@ -455,12 +455,12 @@ describe('User', function () { }); it('actually changed the password (unable to login with old pasword)', async function () { - const [error] = await safe(users.verify(admin.id, admin.password, users.AP_WEBADMIN)); + const [error] = await safe(users.verify(admin.id, admin.password, users.AP_WEBADMIN, {})); expect(error.reason).to.equal(BoxError.INVALID_CREDENTIALS); }); it('actually changed the password (login with new password)', async function () { - await users.verify(admin.id, 'ThisIsNew1Password', users.AP_WEBADMIN); + await users.verify(admin.id, 'ThisIsNew1Password', users.AP_WEBADMIN, {}); }); }); diff --git a/src/users.js b/src/users.js index 9db92cf05..4588d377d 100644 --- a/src/users.js +++ b/src/users.js @@ -331,10 +331,11 @@ async function verifyAppPassword(userId, password, identifier) { } // identifier is only used to check if password is valid for a specific app -async function verify(userId, password, identifier) { +async function verify(userId, password, identifier, options) { assert.strictEqual(typeof userId, 'string'); assert.strictEqual(typeof password, 'string'); assert.strictEqual(typeof identifier, 'string'); + assert.strictEqual(typeof options, 'object'); const user = await get(userId); if (!user) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); @@ -356,42 +357,61 @@ async function verify(userId, password, identifier) { return user; } + const relaxedTotpCheck = !!options.relaxedTotpCheck; // will enforce totp only if totpToken is valid + const totpToken = options.totpToken || null; + if (user.source === 'ldap') { - const ldapUser = await externalLdap.verifyPassword(user, password); - // currently we store twoFactorAuthenticationEnabled in the db as local so amend it to user object - user.twoFactorAuthenticationEnabled = !!ldapUser.twoFactorAuthenticationEnabled; + await externalLdap.verifyPassword(user, password, totpToken); } else { const saltBinary = Buffer.from(user.salt, 'hex'); const [error, derivedKey] = await safe(pbkdf2Async(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST)); if (error) throw new BoxError(BoxError.CRYPTO_ERROR, error); const derivedKeyHex = Buffer.from(derivedKey, 'binary').toString('hex'); - if (derivedKeyHex !== user.password) throw new BoxError(BoxError.INVALID_CREDENTIALS); + if (derivedKeyHex !== user.password) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Username and password does not match'); + + if (user.twoFactorAuthenticationEnabled) { + if (totpToken) { + const verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: totpToken, window: 2 }); + if (!verified) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid totpToken'); + } else if (!relaxedTotpCheck) { + throw new BoxError(BoxError.INVALID_CREDENTIALS, 'A totpToken must be provided'); + } + } } return user; } -async function verifyWithUsername(username, password, identifier) { +async function verifyWithUsername(username, password, identifier, options) { assert.strictEqual(typeof username, 'string'); assert.strictEqual(typeof password, 'string'); assert.strictEqual(typeof identifier, 'string'); + assert.strictEqual(typeof options, 'object'); const user = await getByUsername(username.toLowerCase()); - if (!user) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); + if (user) return await verify(user.id, password, identifier, options); - return await verify(user.id, password, identifier); + const [error, newUserId] = await safe(externalLdap.maybeCreateUser(username.toLowerCase())); + if (error && error.reason === BoxError.BAD_STATE) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); // no external ldap or no auto create + if (error) { + debug(`verifyWithUsername: failed to auto create user ${username}`, error); + throw new BoxError(BoxError.NOT_FOUND, 'User not found'); + } + + return await verify(newUserId, password, identifier, options); } -async function verifyWithEmail(email, password, identifier) { +async function verifyWithEmail(email, password, identifier, options) { assert.strictEqual(typeof email, 'string'); assert.strictEqual(typeof password, 'string'); assert.strictEqual(typeof identifier, 'string'); + assert.strictEqual(typeof options, 'object'); const user = await getByEmail(email.toLowerCase()); if (!user) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); - return await verify(user.id, password, identifier); + return await verify(user.id, password, identifier, options); } async function del(user, auditSource) {