diff --git a/CHANGES b/CHANGES index aec96a792..bfd9f1073 100644 --- a/CHANGES +++ b/CHANGES @@ -2342,4 +2342,5 @@ * Require password for fallback email change * Make password reset logic translatable * support: only verified email address can open support tickets +* Logout users without 2FA when mandatory 2fa is enabled diff --git a/src/settings.js b/src/settings.js index 06d53fc65..ef79ad6a7 100644 --- a/src/settings.js +++ b/src/settings.js @@ -138,8 +138,9 @@ const assert = require('assert'), paths = require('./paths.js'), safe = require('safetydance'), sysinfo = require('./sysinfo.js'), + tokens = require('./tokens.js'), translation = require('./translation.js'), - util = require('util'), + users = require('./users.js'), _ = require('underscore'); const SETTINGS_FIELDS = [ 'name', 'value' ].join(','); @@ -535,7 +536,18 @@ async function setDirectoryConfig(directoryConfig) { if (isDemo()) throw new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'); + const oldConfig = await getDirectoryConfig(); await set(exports.DIRECTORY_CONFIG_KEY, JSON.stringify(directoryConfig)); + + if (directoryConfig.mandatory2FA && !oldConfig.mandatory2FA) { + debug('setDirectoryConfig: logging out non-2FA users to enforce 2FA'); + + const allUsers = await users.list(); + for (const user of allUsers) { + if (!user.twoFactorAuthenticationEnabled) await tokens.delByUserIdAndType(user.id, tokens.ID_WEBADMIN); + } + } + notifyChange(exports.DIRECTORY_CONFIG_KEY, directoryConfig); } diff --git a/src/test/settings-test.js b/src/test/settings-test.js index 823e6c816..ded4942ee 100644 --- a/src/test/settings-test.js +++ b/src/test/settings-test.js @@ -7,10 +7,11 @@ const common = require('./common.js'), expect = require('expect.js'), - settings = require('../settings.js'); + settings = require('../settings.js'), + tokens = require('../tokens.js'); describe('Settings', function () { - const { setup, cleanup } = common; + const { setup, cleanup, admin } = common; before(setup); after(cleanup); @@ -53,6 +54,22 @@ describe('Settings', function () { expect(enabled).to.be(false); }); + it('can get default directory config', async function () { + const directoryConfig = await settings.getDirectoryConfig(); + expect(directoryConfig.lockUserProfiles).to.be(false); + expect(directoryConfig.mandatory2FA).to.be(false); + }); + + it('can set default directory config', async function () { + await tokens.add({ name: 'token1', identifier: admin.id, clientId: tokens.ID_WEBADMIN, expires: Number.MAX_SAFE_INTEGER, lastUsedTime: null, scope: 'unused' }); + let result = await tokens.listByUserId(admin.id); + expect(result.length).to.be(1); // just confirm the token was really added! + + await settings.setDirectoryConfig({ mandatory2FA: true, lockUserProfiles: true }); + result = await tokens.listByUserId(admin.id); + expect(result.length).to.be(0); // should have been removed by mandatory 2fa setting change + }); + it('can get all values', async function () { const allSettings = await settings.list(); expect(allSettings[settings.TIME_ZONE_KEY]).to.be.a('string'); diff --git a/src/test/tokens-test.js b/src/test/tokens-test.js index 39d2d2a9a..f216d04cc 100644 --- a/src/test/tokens-test.js +++ b/src/test/tokens-test.js @@ -119,4 +119,34 @@ describe('Tokens', function () { result = await tokens.getByAccessToken(token1.accessToken); expect(result).to.eql(token1); }); + + it('delByUserIdAndType succeeds', async function () { + const token1 = { + name: 'token1', + identifier: 'user1', + clientId: tokens.ID_WEBADMIN, + expires: Number.MAX_SAFE_INTEGER, + lastUsedTime: null, + scope: 'unused' + }; + const token2 = { + name: 'token2', + identifier: 'user1', + clientId: tokens.ID_SDK, + expires: Date.now(), + lastUsedTime: null + }; + + await tokens.add(token1); + await tokens.add(token2); + + await tokens.delByUserIdAndType('user2', tokens.ID_WEBADMIN); + let result = await tokens.listByUserId('user1'); + expect(result.length).to.be(2); // should not have deleted user1 tokens + + await tokens.delByUserIdAndType('user1', tokens.ID_WEBADMIN); + result = await tokens.listByUserId('user1'); + expect(result.length).to.be(1); + expect(result[0].name).to.be(token2.name); + }); }); diff --git a/src/tokens.js b/src/tokens.js index b69e5b489..6da0a1e3a 100644 --- a/src/tokens.js +++ b/src/tokens.js @@ -8,6 +8,7 @@ exports = module.exports = { delByAccessToken, delExpired, + delByUserIdAndType, listByUserId, getByAccessToken, @@ -15,7 +16,7 @@ exports = module.exports = { validateTokenType, // token client ids. we categorize them so we can have different restrictions based on the client - ID_WEBADMIN: 'cid-webadmin', // dashboard oauth + ID_WEBADMIN: 'cid-webadmin', // dashboard ID_SDK: 'cid-sdk', // created by user via dashboard ID_CLI: 'cid-cli' // created via cli tool }; @@ -104,6 +105,14 @@ async function delExpired() { return result.affectedRows; } +async function delByUserIdAndType(userId, type) { + assert.strictEqual(typeof userId, 'string'); + assert.strictEqual(typeof type, 'string'); + + const result = await database.query('DELETE FROM tokens WHERE identifier=? AND clientId=?', [ userId, type ]); + return result.affectedRows; +} + async function update(id, values) { assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof values, 'object');