Add an option to enforce MFA for all accounts on the instance
This commit is contained in:
parent
1b032a06de
commit
982c15ae0e
11 changed files with 158 additions and 43 deletions
|
@ -127,6 +127,14 @@ module.exports = {
|
|||
*/
|
||||
//otpSessionExpiration: 7*24, // hours
|
||||
|
||||
/* Registered users can be forced to protect their account
|
||||
* with a Multi-factor Authentication (MFA) tool like a TOTP
|
||||
* authenticator application.
|
||||
*
|
||||
* defaults to false
|
||||
*/
|
||||
//enforceMFA: false,
|
||||
|
||||
/* =====================
|
||||
* Admin
|
||||
* ===================== */
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
.cp-loading-container {
|
||||
width: 700px;
|
||||
max-width: 90vw;
|
||||
height: 236px;
|
||||
min-height: 236px;
|
||||
max-height: calc(100vh - 20px);
|
||||
margin: 50px;
|
||||
flex-shrink: 0;
|
||||
|
@ -100,6 +100,7 @@
|
|||
color: @cp_loading-fg;
|
||||
text-align: left;
|
||||
display: none;
|
||||
overflow-y: auto;
|
||||
a {
|
||||
color: @cp_loading-link;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,44 @@
|
|||
}
|
||||
}
|
||||
|
||||
.cp-loading-missing-mfa {
|
||||
.cp-settings-qr-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
.cp-settings-qr-code {
|
||||
input {
|
||||
max-width: 250px;
|
||||
}
|
||||
button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cp-settings-qr {
|
||||
img {
|
||||
border: 10px solid white;
|
||||
border-radius: 10px;
|
||||
}
|
||||
margin: 10px 10px 10px 0;
|
||||
}
|
||||
.cp-password-container {
|
||||
flex-wrap: wrap;
|
||||
gap:0.5rem;
|
||||
|
||||
justify-content:flex-start;
|
||||
input {
|
||||
flex-shrink: 1;
|
||||
max-width: 400px;
|
||||
}
|
||||
label {
|
||||
width: 100%;
|
||||
font-weight: unset;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Properties modal
|
||||
.cp-app-prop {
|
||||
margin-bottom: 10px;
|
||||
|
|
|
@ -224,6 +224,7 @@ module.exports.create = function (config) {
|
|||
commandTimers: {},
|
||||
|
||||
sso: config.sso,
|
||||
enforceMFA: config.enforceMFA,
|
||||
|
||||
// initialized as undefined
|
||||
bearerSecret: void 0,
|
||||
|
|
|
@ -577,7 +577,8 @@ var serveConfig = makeRouteCache(function () {
|
|||
shouldUpdateNode: Env.shouldUpdateNode || undefined,
|
||||
listMyInstance: Env.listMyInstance,
|
||||
accounts_api: Env.accounts_api,
|
||||
sso: ssoCfg
|
||||
sso: ssoCfg,
|
||||
enforceMFA: Env.enforceMFA
|
||||
}, null, '\t'),
|
||||
'});'
|
||||
].join(';\n');
|
||||
|
|
|
@ -4040,5 +4040,21 @@ define([
|
|||
modal = UI.openCustomModal(UI.dialog.customModal(content, {buttons: buttons }));
|
||||
};
|
||||
|
||||
Messages.loading_mfa_required = "Multi-factor Authentication is required on this instance. Please update your account using an anthenticator app and the form below."; // XXX
|
||||
UIElements.onMissingMFA = (common, config, cb) => {
|
||||
let content = h('div');
|
||||
let msg = h('div.cp-loading-missing-mfa', [
|
||||
h('div.alert.alert-warning', Messages.loading_mfa_required),
|
||||
content
|
||||
]);
|
||||
common.totpSetup(config, content, false, (newState) => {
|
||||
if (!newState) {
|
||||
return void UI.errorLoadingScreen(Messages.error);
|
||||
}
|
||||
cb({state: true});
|
||||
});
|
||||
return UI.errorLoadingScreen(msg, false, false);
|
||||
};
|
||||
|
||||
return UIElements;
|
||||
});
|
||||
|
|
|
@ -2160,6 +2160,7 @@ define([
|
|||
// Loading events
|
||||
common.loading = {};
|
||||
common.loading.onDriveEvent = Util.mkEvent();
|
||||
common.loading.onMissingMFAEvent = Util.mkEvent();
|
||||
|
||||
// (Auto)store pads
|
||||
common.autoStore = {};
|
||||
|
@ -2484,6 +2485,22 @@ define([
|
|||
if (AppConfig.beforeLogin) {
|
||||
AppConfig.beforeLogin(LocalStore.isLoggedIn(), waitFor());
|
||||
}
|
||||
}).nThen(function (waitFor) {
|
||||
var blockHash = LocalStore.getBlockHash();
|
||||
if (!blockHash || !Config.enforceMFA) { return; }
|
||||
|
||||
// If this instance is configured to enforce MFA for all registered users,
|
||||
// request the login block with no credential to check if it is protected.
|
||||
var parsed = Block.parseBlockHash(blockHash);
|
||||
Util.getBlock(parsed.href, { }, waitFor((err, response) => {
|
||||
// If this account is already protected, nothing to do
|
||||
if (err === 401 && response.method) { return; }
|
||||
|
||||
// Missing MFA protection, show set up screen
|
||||
common.loading.onMissingMFAEvent.fire({
|
||||
cb: waitFor()
|
||||
});
|
||||
}));
|
||||
|
||||
}).nThen(function (waitFor) {
|
||||
// if a block URL is present then the user is probably logged in with a modern account
|
||||
|
|
|
@ -226,6 +226,61 @@ define([
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
var addFirstHandlers = () => {
|
||||
sframeChan.on('Q_SETTINGS_CHECK_PASSWORD', function (data, cb) {
|
||||
var blockHash = Utils.LocalStore.getBlockHash();
|
||||
var userHash = Utils.LocalStore.getUserHash();
|
||||
var correct = (blockHash && blockHash === data.blockHash) ||
|
||||
(!blockHash && userHash === data.userHash);
|
||||
cb({correct: correct});
|
||||
});
|
||||
sframeChan.on('Q_SETTINGS_TOTP_SETUP', function (obj, cb) {
|
||||
require([
|
||||
'/common/outer/http-command.js',
|
||||
], function (ServerCommand) {
|
||||
var data = obj.data;
|
||||
data.command = 'TOTP_SETUP';
|
||||
data.session = Utils.LocalStore.getSessionToken();
|
||||
ServerCommand(obj.key, data, function (err, response) {
|
||||
cb({ success: Boolean(!err && response && response.bearer) });
|
||||
if (response && response.bearer) {
|
||||
Utils.LocalStore.setSessionToken(response.bearer);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
sframeChan.on('Q_SETTINGS_TOTP_REVOKE', function (obj, cb) {
|
||||
require([
|
||||
'/common/outer/http-command.js',
|
||||
], function (ServerCommand) {
|
||||
ServerCommand(obj.key, obj.data, function (err, response) {
|
||||
cb({ success: Boolean(!err && response && response.success) });
|
||||
if (response && response.success) {
|
||||
Utils.LocalStore.setSessionToken('');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
sframeChan.on('Q_SETTINGS_GET_SSO_SEED', function (obj, _cb) {
|
||||
var cb = Utils.Util.mkAsync(_cb);
|
||||
cb({
|
||||
seed: Utils.LocalStore.getSSOSeed()
|
||||
});
|
||||
});
|
||||
Cryptpad.loading.onMissingMFAEvent.reg((data) => {
|
||||
var cb = data.cb;
|
||||
if (!sframeChan) { return void cb('EINVAL'); }
|
||||
sframeChan.query('Q_LOADING_MISSING_AUTH', {
|
||||
accountName: Utils.LocalStore.getAccountName(),
|
||||
origin: window.location.origin,
|
||||
}, (err, obj) => {
|
||||
if (obj && obj.state) { return void cb(true); }
|
||||
console.error(err || obj);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var whenReady = waitFor(function (msg) {
|
||||
if (msg.source !== iframe) { return; }
|
||||
var data = typeof(msg.data) === "string" ? JSON.parse(msg.data) : msg.data;
|
||||
|
@ -242,6 +297,7 @@ define([
|
|||
});
|
||||
SFrameChannel.create(msgEv, postMsg, waitFor(function (sfc) {
|
||||
Utils.sframeChan = sframeChan = sfc;
|
||||
addFirstHandlers();
|
||||
window.CryptPad_loadingError = function (e) {
|
||||
sfc.event('EV_LOADING_ERROR', e);
|
||||
};
|
||||
|
@ -267,6 +323,7 @@ define([
|
|||
}
|
||||
} catch (e) { console.error(e); }
|
||||
|
||||
|
||||
// NOTE: Driveless mode should only work for existing pads, but we can't check that
|
||||
// before creating the worker because we need the anon RPC to do so.
|
||||
// We're only going to check if a hash exists in the URL or not.
|
||||
|
|
|
@ -15,6 +15,7 @@ define([
|
|||
'/common/sframe-common-mailbox.js',
|
||||
'/common/inner/cache.js',
|
||||
'/common/inner/common-mediatag.js',
|
||||
'/common/inner/mfa.js',
|
||||
'/common/metadata-manager.js',
|
||||
|
||||
'/customize/application_config.js',
|
||||
|
@ -46,6 +47,7 @@ define([
|
|||
Mailbox,
|
||||
Cache,
|
||||
MT,
|
||||
MFA,
|
||||
MetadataMgr,
|
||||
AppConfig,
|
||||
Pages,
|
||||
|
@ -119,6 +121,7 @@ define([
|
|||
funcs.importMediaTagMenu = callWithCommon(MT.importMediaTagMenu);
|
||||
funcs.getMediaTagPreview = callWithCommon(MT.getMediaTagPreview);
|
||||
funcs.getMediaTag = callWithCommon(MT.getMediaTag);
|
||||
funcs.totpSetup = callWithCommon(MFA.totpSetup);
|
||||
|
||||
// Thumb
|
||||
funcs.displayThumbnail = callWithCommon(Thumb.displayThumbnail);
|
||||
|
@ -889,6 +892,10 @@ define([
|
|||
UI.updateLoadingProgress(data);
|
||||
});
|
||||
|
||||
ctx.sframeChan.on('Q_LOADING_MISSING_AUTH', function (data, cb) {
|
||||
UIElements.onMissingMFA(funcs, data, cb);
|
||||
});
|
||||
|
||||
ctx.sframeChan.on('EV_NEW_VERSION', function () {
|
||||
// TODO lock the UI and do the same in non-framework apps
|
||||
var $err = $('<div>').append(Messages.newVersionError);
|
||||
|
|
|
@ -1236,7 +1236,16 @@ define([
|
|||
sframeChan.query('Q_SETTINGS_MFA_CHECK', {}, function (err, obj) {
|
||||
if (err || !obj || (obj && obj.err === 'NOBLOCK')) { return void cb(false); }
|
||||
var enabled = obj && obj.mfa && obj.type === 'TOTP';
|
||||
drawMfa(content, Boolean(enabled));
|
||||
var config = {
|
||||
accountName: privateData.accountName,
|
||||
origin: privateData.origin
|
||||
};
|
||||
var draw = (state) => {
|
||||
common.totpSetup(config, content, state, (newState) => {
|
||||
draw(newState);
|
||||
});
|
||||
};
|
||||
draw(Boolean(enabled));
|
||||
cb(content);
|
||||
});
|
||||
}, true);
|
||||
|
|
|
@ -70,40 +70,6 @@ define([
|
|||
sframeChan.on('Q_SETTINGS_IMPORT_LOCAL', function (data, cb) {
|
||||
Cryptpad.mergeAnonDrive(cb);
|
||||
});
|
||||
sframeChan.on('Q_SETTINGS_CHECK_PASSWORD', function (data, cb) {
|
||||
var blockHash = Utils.LocalStore.getBlockHash();
|
||||
var userHash = Utils.LocalStore.getUserHash();
|
||||
var correct = (blockHash && blockHash === data.blockHash) ||
|
||||
(!blockHash && userHash === data.userHash);
|
||||
cb({correct: correct});
|
||||
});
|
||||
sframeChan.on('Q_SETTINGS_TOTP_SETUP', function (obj, cb) {
|
||||
require([
|
||||
'/common/outer/http-command.js',
|
||||
], function (ServerCommand) {
|
||||
var data = obj.data;
|
||||
data.command = 'TOTP_SETUP';
|
||||
data.session = Utils.LocalStore.getSessionToken();
|
||||
ServerCommand(obj.key, data, function (err, response) {
|
||||
cb({ success: Boolean(!err && response && response.bearer) });
|
||||
if (response && response.bearer) {
|
||||
Utils.LocalStore.setSessionToken(response.bearer);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
sframeChan.on('Q_SETTINGS_TOTP_REVOKE', function (obj, cb) {
|
||||
require([
|
||||
'/common/outer/http-command.js',
|
||||
], function (ServerCommand) {
|
||||
ServerCommand(obj.key, obj.data, function (err, response) {
|
||||
cb({ success: Boolean(!err && response && response.success) });
|
||||
if (response && response.success) {
|
||||
Utils.LocalStore.setSessionToken('');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
sframeChan.on('Q_SETTINGS_MFA_CHECK', function (obj, cb) {
|
||||
require([
|
||||
'/common/outer/login-block.js',
|
||||
|
@ -120,12 +86,6 @@ define([
|
|||
});
|
||||
});
|
||||
});
|
||||
sframeChan.on('Q_SETTINGS_GET_SSO_SEED', function (obj, _cb) {
|
||||
var cb = Utils.Util.mkAsync(_cb);
|
||||
cb({
|
||||
seed: Utils.LocalStore.getSSOSeed()
|
||||
});
|
||||
});
|
||||
sframeChan.on('Q_SETTINGS_REMOVE_OWNED_PADS', function (data, cb) {
|
||||
Cryptpad.removeOwnedPads(data, cb);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue