Auth: Disable TOTP/MFA from the admin panel

This commit is contained in:
yflory 2023-05-16 15:11:43 +02:00
parent 4b51300174
commit 36a1c604d8
4 changed files with 207 additions and 27 deletions

View File

@ -442,34 +442,8 @@ So, we should:
1. Revoke the TOTP authentication for their block
2. Remove all existing sessions
*/
var { publicKey } = body;
nThen(function (w) {
MFA.delete(Env, publicKey, w(function (err) {
if (!err) { return; }
w.abort();
Env.Log.error('TOTP_REVOKE_MFA_DELETE', {
error: err,
publicKey: publicKey,
});
cb('MFA_ERROR');
}));
}).nThen(function () {
Sessions.deleteUser(Env, publicKey, function (err) {
if (!err) { return; }
// If we can't delete the sessions, don't send an erorr, just log to the server.
// The MFA will still be correctly disabled as long as the first step is done.
Env.Log.error('TOTP_REVOKE_SESSIONS__DELETE', {
error: err,
publicKey: publicKey,
});
});
}).nThen(function () {
cb(void 0, {
success: true
});
});
MFA.revoke(Env, publicKey, cb);
};

View File

@ -9,6 +9,7 @@ const Pinning = require("./pin-rpc");
const Core = require("./core");
const Channel = require("./channel");
const BlockStore = require("../storage/block");
const MFA = require("../storage/mfa");
var Fs = require("fs");
@ -498,6 +499,19 @@ var getDocumentStatus = function (Env, Server, cb, data) {
}
response.archived = result;
}));
MFA.read(Env, id, w(function (err, v) {
if (err === 'ENOENT') {
response.totp = 'DISABLED';
} else if (v) {
var parsed = Util.tryParse(v);
response.totp = {
enabled: true,
recovery: parsed.contact && parsed.contact.split(':')[0]
};
} else {
response.totp = err
}
}));
}).nThen(function () {
cb(void 0, response);
});
@ -539,6 +553,12 @@ var getDocumentStatus = function (Env, Server, cb, data) {
});
};
var disableMFA = function (Env, Server, cb, data) {
var id = Array.isArray(data) && data[1];
if (typeof(id) !== 'string' || id.length !== 44) { return void cb("EINVAL"); }
MFA.revoke(Env, id, cb);
};
var getPinList = function (Env, Server, cb, data) {
var key = Array.isArray(data) && data[1];
if (!isValidKey(key)) { return void cb("EINVAL"); }
@ -746,6 +766,8 @@ var commands = {
GET_LAST_CHANNEL_TIME: getLastChannelTime,
GET_DOCUMENT_STATUS: getDocumentStatus,
DISABLE_MFA: disableMFA,
GET_PIN_LIST: getPinList,
GET_PIN_HISTORY: getPinHistory,
ARCHIVE_PIN_LOG: archivePinLog,

View File

@ -1,6 +1,8 @@
const Basic = require("./basic");
const Path = require("node:path");
const Util = require("../common-util");
const Sessions = require("./sessions");
const nThen = require("nthen");
const MFA = module.exports;
@ -41,3 +43,32 @@ MFA.delete = function (Env, id, cb) {
Basic.delete(Env, path, cb);
};
MFA.revoke = function (Env, publicKey, cb) {
nThen(function (w) {
MFA.delete(Env, publicKey, w(function (err) {
if (!err) { return; }
w.abort();
Env.Log.error('TOTP_REVOKE_MFA_DELETE', {
error: err,
publicKey: publicKey,
});
cb('MFA_ERROR');
}));
}).nThen(function () {
Sessions.deleteUser(Env, publicKey, function (err) {
if (!err) { return; }
// If we can't delete the sessions, don't send an erorr, just log to the server.
// The MFA will still be correctly disabled as long as the first step is done.
Env.Log.error('TOTP_REVOKE_SESSIONS__DELETE', {
error: err,
publicKey: publicKey,
});
});
}).nThen(function () {
cb(void 0, {
success: true
});
});
};

View File

@ -15,6 +15,7 @@ define([
'/common/common-signing-keys.js',
'/support/ui.js',
'/common/clipboard.js',
'json.sortify',
'/lib/datepicker/flatpickr.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
@ -40,6 +41,7 @@ define([
Keys,
Support,
Clipboard,
Sortify,
Flatpickr
)
{
@ -75,6 +77,7 @@ define([
'cp-admin-account-metadata',
'cp-admin-document-metadata',
'cp-admin-block-metadata',
'cp-admin-totp-recovery',
],
'stats': [ // Msg.admin_cat_stats
'cp-admin-refresh-stats',
@ -1071,6 +1074,7 @@ define([
}
data.live = res[0].live;
data.archived = res[0].archived;
data.totp = res[0].totp;
}));
}).nThen(function () {
try {
@ -1094,6 +1098,9 @@ define([
row(Messages.admin_blockAvailable, localizeState(data.live));
row(Messages.admin_blockArchived, localizeState(data.archived));
row(Messages.admin_totpEnabled, localizeState(data.totp.enabled));
row(Messages.admin_totpRecoveryMethod, data.totp.recovery); // XXX localize?
if (data.live) {
var archiveButton = danger(Messages.ui_archive, function () {
justifyArchivalDialog('', reason => {
@ -1228,6 +1235,152 @@ define([
return $div;
};
Messages.admin_totpEnabled = "TOTP is enabled"; // XXX
Messages.admin_totpRecoveryMethod = "TOTP recovery method"; // XXX
Messages.admin_totpFailed = "Signature verification failed";
Messages.admin_totpCheck = "Signature verification success";
Messages.admin_totpDisable = "Disable TOTP for this account";
Messages.admin_totpDisableButton = "Disable";
var renderTOTPData = function (data) {
var tableObj = makeMetadataTable('cp-block-stats');
var row = tableObj.row;
row(Messages.admin_generatedAt, maybeDate(data.generated));
row(Messages.admin_blockKey, h('code', data.key));
row(Messages.admin_blockAvailable, localizeState(data.live));
if (!data.live || !data.totp) { return tableObj.table; }
row(Messages.admin_totpCheck, localizeState(data.totpCheck));
if (!data.totpCheck) { return tableObj.table; }
row(Messages.admin_totpEnabled, localizeState(Boolean(data.totp.enabled)));
if (data.totp && data.totp.enabled) {
row(Messages.admin_totpRecoveryMethod, data.totp.recovery); // XXX localize?
}
if (!data.totpCheck || !data.totp.enabled) { return tableObj.table; }
// TOTP is enabled and the signature is correct: display "disable TOTP" button
var disableButton = h('button.btn.btn-danger', Messages.admin_totpDisableButton);
UI.confirmButton(disableButton, { classes: 'btn-danger' }, function () {
sframeCommand('DISABLE_MFA', data.key, (err, res) => {
if (err) {
console.error(err);
return void UI.warn(Messages.error);
}
if (!Array.isArray(res) || !res[0] || !res[0].success) {
return UI.warn(Messages.error);
}
UI.log(Messages.ui_success);
});
});
row(Messages.admin_totpDisable, disableButton);
return tableObj.table;
};
var checkTOTPRequest = function (json) {
var clone = Util.clone(json);
delete clone.proof;
var msg = Nacl.util.decodeUTF8(Sortify(clone));
var sig = Nacl.util.decodeBase64(json.proof);
var pub = Nacl.util.decodeBase64(json.blockId)
return Nacl.sign.detached.verify(msg, sig, pub);
};
create['totp-recovery'] = function () {
var key = 'totp-recovery';
// XXX translation keys
var $div = makeBlock(key, true); // Msg.admin_totpRecoveryHint.totpRecoveryTitle
var textarea = h('textarea', {});
var $input = $(textarea);
var box = h('div.cp-admin-setter', [
textarea,
]);
$div.find('.cp-sidebarlayout-description').after(box);
var results = h('span');
$div.append(results);
var $btn = $div.find('.btn');
$btn.text(Messages.ui_generateReport);
disable($btn);
var pending = false;
var getInputState = function () {
var val = $input.val().trim();
var state = {
pending: pending,
value: undefined,
key: '',
};
var json;
try { json = JSON.parse(val); } catch (err) { }
/*
Example
{
"intent": "Disable TOTP",
"date": "2023-05-15T15:38:40.916Z",
"blockId": "+0PdpTuQi9/O2qjoJ8FLcvPEwChLfDWJrYXyPdVGzOo=",
"proof": "iDcHy6+ymiyWzK/oYNPQ1ItFNCiTmmJuAyYmcEXNha2U1nUxyBWAf0o7ZXWhygS6XI5BLrjH+DDcbWitfO3bCg=="
}
*/
if (!json || json.intent !== "Disable TOTP" || !json.blockId || json.blockId.length !== 44 ||
!json.date || !json.proof) { return state; }
state.value = json;
state.key = json.blockId.replace(/\//g, '-');
return state;
};
var setInterfaceState = function () {
var state = getInputState();
var all = [$btn, $input];
if (state.pending) {
all.forEach(disable);
} else {
all.forEach(enable);
}
};
setInterfaceState();
$btn.click(function () {
if (pending) { return; }
var state = getInputState();
if (!state.value) { return; }
pending = true;
setInterfaceState();
getBlockData(state.key, (err, data) => {
pending = false;
setInterfaceState();
console.warn(data);
if (err || !data) {
results.innerHTML = '';
console.log(err, data);
return UI.warn(Messages.error);
}
var check = checkTOTPRequest(state.value);
if (!check) { UI.warn(Messages.admin_totpFailed); }
data.totpCheck = check;
var table = renderTOTPData(data);
results.innerHTML = '';
results.appendChild(table);
});
});
return $div;
};
var makeAdminCheckbox = function (data) {
return function () {
var state = data.getState();