Good progress on 2FA/OTP config
This commit is contained in:
parent
b62f55961f
commit
8802ae24ba
|
@ -16,6 +16,7 @@ const { getPredefinedMCIValue } = require('../core/predefined_mci.js');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const iconvDecode = require('iconv-lite').decode;
|
||||||
|
|
||||||
exports.MenuModule = class MenuModule extends PluginModule {
|
exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
|
|
||||||
|
@ -379,7 +380,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
displayAsset(name, options, cb) {
|
displayAsset(nameOrData, options, cb) {
|
||||||
if(_.isFunction(options)) {
|
if(_.isFunction(options)) {
|
||||||
cb = options;
|
cb = options;
|
||||||
options = {};
|
options = {};
|
||||||
|
@ -389,10 +390,25 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
this.client.term.rawWrite(ansi.resetScreen());
|
this.client.term.rawWrite(ansi.resetScreen());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options = Object.assign( { client : this.client, font : this.menuConfig.config.font }, options );
|
||||||
|
|
||||||
|
if(Buffer.isBuffer(nameOrData)) {
|
||||||
|
const data = iconvDecode(nameOrData, options.encoding || 'cp437');
|
||||||
|
return theme.displayPreparedArt(
|
||||||
|
options,
|
||||||
|
{ data },
|
||||||
|
(err, artData) => {
|
||||||
|
if(cb) {
|
||||||
|
return cb(err, artData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return theme.displayThemedAsset(
|
return theme.displayThemedAsset(
|
||||||
name,
|
nameOrData,
|
||||||
this.client,
|
this.client,
|
||||||
Object.assign( { font : this.menuConfig.config.font }, options ),
|
options,
|
||||||
(err, artData) => {
|
(err, artData) => {
|
||||||
if(cb) {
|
if(cb) {
|
||||||
return cb(err, artData);
|
return cb(err, artData);
|
||||||
|
@ -513,7 +529,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
setViewText(formName, mciId, text, appendMultiLine) {
|
setViewText(formName, mciId, text, appendMultiLine) {
|
||||||
const view = this.viewControllers[formName].getView(mciId);
|
const view = this.getView(formName, mciId);
|
||||||
if(!view) {
|
if(!view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -525,6 +541,11 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getView(formName, id) {
|
||||||
|
const form = this.viewControllers[formName];
|
||||||
|
return form && form.getView(id);
|
||||||
|
}
|
||||||
|
|
||||||
updateCustomViewTextsWithFilter(formName, startId, fmtObj, options) {
|
updateCustomViewTextsWithFilter(formName, startId, fmtObj, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
|
|
|
@ -372,12 +372,28 @@ function twoFactorAuthOTP(user) {
|
||||||
prepareOTP,
|
prepareOTP,
|
||||||
} = require('../../core/user_2fa_otp.js');
|
} = require('../../core/user_2fa_otp.js');
|
||||||
|
|
||||||
|
let otpType = argv._[argv._.length - 1];
|
||||||
|
|
||||||
|
// shortcut for removal
|
||||||
|
if('disable' === otpType) {
|
||||||
|
const props = [
|
||||||
|
UserProps.AuthFactor2OTP,
|
||||||
|
UserProps.AuthFactor2OTPSecret,
|
||||||
|
UserProps.AuthFactor2OTPBackupCodes,
|
||||||
|
];
|
||||||
|
return user.removeProperties(props, err => {
|
||||||
|
if(err) {
|
||||||
|
console.error(err.message);
|
||||||
|
} else {
|
||||||
|
console.info(`2FA OTP disabled for ${user.username}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function validate(callback) {
|
function validate(callback) {
|
||||||
// :TODO: Prompt for if not supplied
|
// :TODO: Prompt for if not supplied
|
||||||
let otpType = argv._[argv._.length - 1];
|
|
||||||
|
|
||||||
// allow aliases for OTP types
|
// allow aliases for OTP types
|
||||||
otpType = {
|
otpType = {
|
||||||
google : OTPTypes.GoogleAuthenticator,
|
google : OTPTypes.GoogleAuthenticator,
|
||||||
|
|
|
@ -35,11 +35,16 @@ util.inherits(ToggleMenuView, MenuView);
|
||||||
ToggleMenuView.prototype.redraw = function() {
|
ToggleMenuView.prototype.redraw = function() {
|
||||||
ToggleMenuView.super_.prototype.redraw.call(this);
|
ToggleMenuView.super_.prototype.redraw.call(this);
|
||||||
|
|
||||||
|
if(0 === this.items.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//this.cachePositions();
|
//this.cachePositions();
|
||||||
|
|
||||||
this.client.term.write(this.hasFocus ? this.getFocusSGR() : this.getSGR());
|
this.client.term.write(this.hasFocus ? this.getFocusSGR() : this.getSGR());
|
||||||
|
|
||||||
assert(this.items.length === 2);
|
assert(this.items.length === 2, 'ToggleMenuView must contain exactly (2) items');
|
||||||
|
|
||||||
for(var i = 0; i < 2; i++) {
|
for(var i = 0; i < 2; i++) {
|
||||||
var item = this.items[i];
|
var item = this.items[i];
|
||||||
var text = strUtil.stylizeString(
|
var text = strUtil.stylizeString(
|
||||||
|
@ -102,7 +107,7 @@ ToggleMenuView.prototype.onKeyPress = function(ch, key) {
|
||||||
if(key) {
|
if(key) {
|
||||||
if(this.isKeyMapped('right', key.name) || this.isKeyMapped('down', key.name)) {
|
if(this.isKeyMapped('right', key.name) || this.isKeyMapped('down', key.name)) {
|
||||||
this.focusNext();
|
this.focusNext();
|
||||||
} else if(this.isKeyMapped('left', key.name) || this.isKeyMapped('up', key.nam4e)) {
|
} else if(this.isKeyMapped('left', key.name) || this.isKeyMapped('up', key.name)) {
|
||||||
this.focusPrevious();
|
this.focusPrevious();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,11 @@ const Config = require('./config.js').get;
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const async = require('async');
|
|
||||||
const qrGen = require('qrcode-generator');
|
const qrGen = require('qrcode-generator');
|
||||||
|
|
||||||
exports.prepareOTP = prepareOTP;
|
exports.prepareOTP = prepareOTP;
|
||||||
|
exports.createQRCode = createQRCode;
|
||||||
|
exports.otpFromType = otpFromType;
|
||||||
exports.loginFactor2_OTP = loginFactor2_OTP;
|
exports.loginFactor2_OTP = loginFactor2_OTP;
|
||||||
|
|
||||||
const OTPTypes = exports.OTPTypes = {
|
const OTPTypes = exports.OTPTypes = {
|
||||||
|
|
|
@ -0,0 +1,171 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ENiGMA½
|
||||||
|
const { MenuModule } = require('./menu_module.js');
|
||||||
|
const UserProps = require('./user_property.js');
|
||||||
|
const {
|
||||||
|
OTPTypes,
|
||||||
|
otpFromType,
|
||||||
|
createQRCode,
|
||||||
|
} = require('./user_2fa_otp.js');
|
||||||
|
|
||||||
|
// deps
|
||||||
|
const async = require('async');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const iconv = require('iconv-lite');
|
||||||
|
|
||||||
|
exports.moduleInfo = {
|
||||||
|
name : 'User 2FA/OTP Configuration',
|
||||||
|
desc : 'Module for user 2FA/OTP configuration',
|
||||||
|
author : 'NuSkooler',
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormIds = {
|
||||||
|
menu : 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MciViewIds = {
|
||||||
|
enableToggle : 1,
|
||||||
|
typeSelection : 2,
|
||||||
|
submission : 3,
|
||||||
|
infoText : 4,
|
||||||
|
|
||||||
|
customRangeStart : 10, // 10+ = customs
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs });
|
||||||
|
|
||||||
|
this.menuMethods = {
|
||||||
|
showQRCode : (formData, extraArgs, cb) => {
|
||||||
|
return this.showQRCode(cb);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mciReady(mciData, cb) {
|
||||||
|
super.mciReady(mciData, err => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
(callback) => {
|
||||||
|
return this.prepViewController('menu', FormIds.menu, mciData.menu, callback);
|
||||||
|
},
|
||||||
|
(callback) => {
|
||||||
|
const requiredCodes = [
|
||||||
|
MciViewIds.enableToggle,
|
||||||
|
MciViewIds.typeSelection,
|
||||||
|
MciViewIds.submission,
|
||||||
|
];
|
||||||
|
return this.validateMCIByViewIds('menu', requiredCodes, callback);
|
||||||
|
},
|
||||||
|
(callback) => {
|
||||||
|
const enableToggleView = this.getView('menu', MciViewIds.enableToggle);
|
||||||
|
let initialIndex = this.isOTPEnabledForUser() ? 1 : 0;
|
||||||
|
enableToggleView.setFocusItemIndex(initialIndex);
|
||||||
|
this.enableToggleUpdate(initialIndex);
|
||||||
|
|
||||||
|
enableToggleView.on('index update', idx => {
|
||||||
|
return this.enableToggleUpdate(idx);
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeSelectionView = this.getView('menu', MciViewIds.typeSelection);
|
||||||
|
initialIndex = this.typeSelectionIndexFromUserOTPType();
|
||||||
|
typeSelectionView.setFocusItemIndex(initialIndex);
|
||||||
|
|
||||||
|
typeSelectionView.on('index update', idx => {
|
||||||
|
return this.typeSelectionUpdate(idx);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.viewControllers.menu.on('return', view => {
|
||||||
|
if(view === enableToggleView) {
|
||||||
|
return this.enableToggleUpdate(enableToggleView.focusedItemIndex);
|
||||||
|
} else if (view === typeSelectionView) {
|
||||||
|
return this.typeSelectionUpdate(typeSelectionView.focusedItemIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showQRCode(cb) {
|
||||||
|
const otp = otpFromType(this.client.user.getProperty(UserProps.AuthFactor2OTP));
|
||||||
|
let qrCodeAscii = '';
|
||||||
|
if(!otp) {
|
||||||
|
qrCodeAscii = '2FA/OTP is not currently enabled for this account';
|
||||||
|
}
|
||||||
|
|
||||||
|
const qrOptions = {
|
||||||
|
username : this.client.user.username,
|
||||||
|
qrType : 'ascii',
|
||||||
|
};
|
||||||
|
qrCodeAscii = createQRCode(
|
||||||
|
otp,
|
||||||
|
qrOptions,
|
||||||
|
this.client.user.getProperty(UserProps.AuthFactor2OTPSecret)
|
||||||
|
).replace(/\n/g, '\r\n');
|
||||||
|
|
||||||
|
const modOpts = {
|
||||||
|
extraArgs : {
|
||||||
|
artData : iconv.encode(`${qrCodeAscii}\r\n`, 'cp437'),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.gotoMenu(
|
||||||
|
this.menuConfig.config.mainMenuUser2FAOTP_ShowQR || 'mainMenuUser2FAOTP_ShowQR',
|
||||||
|
modOpts,
|
||||||
|
cb
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isOTPEnabledForUser() {
|
||||||
|
return this.typeSelectionIndexFromUserOTPType(-1) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getInfoText(key) {
|
||||||
|
return _.get(this.config, [ 'infoText', key ], '');
|
||||||
|
}
|
||||||
|
|
||||||
|
enableToggleUpdate(idx) {
|
||||||
|
const key = {
|
||||||
|
0 : '2faDisabled',
|
||||||
|
1 : '2faEnabled',
|
||||||
|
}[idx];
|
||||||
|
this.updateCustomViewTextsWithFilter('menu', MciViewIds.customRangeStart, { infoText : this.getInfoText(key) } );
|
||||||
|
}
|
||||||
|
|
||||||
|
typeSelectionIndexFromUserOTPType(defaultIndex = 0) {
|
||||||
|
const type = this.client.user.getProperty(UserProps.AuthFactor2OTP);
|
||||||
|
return {
|
||||||
|
[ OTPTypes.RFC6238_TOTP ] : 0,
|
||||||
|
[ OTPTypes.RFC4266_HOTP ] : 1,
|
||||||
|
[ OTPTypes.GoogleAuthenticator ] : 2,
|
||||||
|
}[type] || defaultIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
otpTypeFromTypeSelectionIndex(idx) {
|
||||||
|
return {
|
||||||
|
0 : OTPTypes.RFC6238_TOTP,
|
||||||
|
1 : OTPTypes.RFC4266_HOTP,
|
||||||
|
2 : OTPTypes.GoogleAuthenticator,
|
||||||
|
}[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
typeSelectionUpdate(idx) {
|
||||||
|
const key = '2faType_' + this.otpTypeFromTypeSelectionIndex(idx);
|
||||||
|
this.updateCustomViewTextsWithFilter('menu', MciViewIds.customRangeStart, { infoText : this.getInfoText(key) } );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -60,7 +60,7 @@ module.exports = {
|
||||||
|
|
||||||
SSHPubKey : 'ssh_public_key', // OpenSSH format (ssh-keygen, etc.)
|
SSHPubKey : 'ssh_public_key', // OpenSSH format (ssh-keygen, etc.)
|
||||||
AuthFactor1Types : 'auth_factor1_types', // List of User.AuthFactor1Types value(s)
|
AuthFactor1Types : 'auth_factor1_types', // List of User.AuthFactor1Types value(s)
|
||||||
AuthFactor2OTP : 'auth_factor2_otp', // If present, OTP type for 2FA
|
AuthFactor2OTP : 'auth_factor2_otp', // If present, OTP type for 2FA. See OTPTypes
|
||||||
AuthFactor2OTPSecret : 'auth_factor2_otp_secret', // Secret used in conjunction with OTP 2FA
|
AuthFactor2OTPSecret : 'auth_factor2_otp_secret', // Secret used in conjunction with OTP 2FA
|
||||||
AuthFactor2OTPBackupCodes : 'auth_factor2_otp_backup', // JSON array of backup codes
|
AuthFactor2OTPBackupCodes : 'auth_factor2_otp_backup', // JSON array of backup codes
|
||||||
};
|
};
|
||||||
|
|
|
@ -178,6 +178,12 @@ View.prototype.setSpecialKeyMapOverride = function(specialKeyMapOverride) {
|
||||||
|
|
||||||
View.prototype.setPropertyValue = function(propName, value) {
|
View.prototype.setPropertyValue = function(propName, value) {
|
||||||
switch(propName) {
|
switch(propName) {
|
||||||
|
case 'acceptsFocus' :
|
||||||
|
if (_.isBoolean(value)) {
|
||||||
|
this.acceptsFocus = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'height' : this.setHeight(value); break;
|
case 'height' : this.setHeight(value); break;
|
||||||
case 'width' : this.setWidth(value); break;
|
case 'width' : this.setWidth(value); break;
|
||||||
case 'focus' : this.setFocus(value); break;
|
case 'focus' : this.setFocus(value); break;
|
||||||
|
|
Loading…
Reference in New Issue