From 2b154800c06d6ebceede590ab946d428c6d0c184 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 13 Jun 2019 22:54:56 -0600 Subject: [PATCH] More OTP updates/fixes * Ensure email address at save * Fix QR display issues for non-Google Auth * More cleanup/etc. --- core/user_2fa_otp.js | 2 +- core/user_2fa_otp_config.js | 75 +++++++++++++++++++++++++++---- core/user_2fa_otp_web_register.js | 20 +++++++-- www/otp_register.template.html | 10 +++-- 4 files changed, 90 insertions(+), 17 deletions(-) diff --git a/core/user_2fa_otp.js b/core/user_2fa_otp.js index 6d5ae6e1..c51d1282 100644 --- a/core/user_2fa_otp.js +++ b/core/user_2fa_otp.js @@ -123,7 +123,7 @@ function createQRCode(otp, options, secret) { svg : qrCode.createSvgTag, }[options.qrType](options.cellSize); } catch(e) { - return; + return ''; } } diff --git a/core/user_2fa_otp_config.js b/core/user_2fa_otp_config.js index 09a72a20..136c9178 100644 --- a/core/user_2fa_otp_config.js +++ b/core/user_2fa_otp_config.js @@ -8,11 +8,11 @@ const { OTPTypes, otpFromType, createQRCode, + createBackupCodes, } = require('./user_2fa_otp.js'); const { Errors } = require('./enig_error.js'); const { getServer } = require('./listening_server.js'); const WebServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; -const Config = require('./config.js').get; const WebRegister = require('./user_2fa_otp_web_register.js'); // deps @@ -42,6 +42,11 @@ const MciViewIds = { const DefaultMsg = { otpNotEnabled : '2FA/OTP is not currently enabled for this account.', noBackupCodes : 'No backup codes remaining or set.', + saveDisabled : '2FA/OTP is now disabled for this account.', + saveEmailSent : 'An 2FA/OTP registration email has been sent with further instructions.', + saveError : 'Failed to send email. Please contact the system operator.', + qrNotAvail : 'QR code not available for this OTP type.', + emailRequired : 'Your account must have a valid email address set to use this feature.', }; exports.getModule = class User2FA_OTPConfigModule extends MenuModule { @@ -59,6 +64,9 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { showBackupCodes : (formData, extraArgs, cb) => { return this.showBackupCodes(cb); }, + generateNewBackupCodes : (formData, extraArgs, cb) => { + return this.generateNewBackupCodes(cb); + }, saveChanges : (formData, extraArgs, cb) => { return this.saveChanges(formData, cb); } @@ -153,11 +161,18 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { username : this.client.user.username, qrType : 'ascii', }; + qrCode = createQRCode( otp, qrOptions, this.client.user.getProperty(UserProps.AuthFactor2OTPSecret) - ).replace(/\n/g, '\r\n'); + ); + + if(qrCode) { + qrCode = qrCode.replace(/\n/g, '\r\n'); + } else { + qrCode = this.config.qrNotAvail || DefaultMsg.qrNotAvail; + } } return this.displayDetails(qrCode, cb); @@ -186,24 +201,66 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { return this.displayDetails(info, cb); } + generateNewBackupCodes(cb) { + if(!this.isOTPEnabledForUser()) { + const info = this.config.otpNotEnabled || DefaultMsg.otpNotEnabled; + return this.displayDetails(info, cb); + } + + const backupCodes = createBackupCodes(); + this.client.user.persistProperty( + UserProps.AuthFactor2OTPBackupCodes, + JSON.stringify(backupCodes), + err => { + if(err) { + return cb(err); + } + const info = backupCodes.join(', '); + return this.displayDetails(info, cb); + } + ); + } + saveChanges(formData, cb) { const enabled = 1 === _.get(formData, 'value.enableToggle', 0); return enabled ? this.saveChangesEnable(formData, cb) : this.saveChangesDisable(cb); } saveChangesEnable(formData, cb) { + // User must have an email address set to save + const emailRegExp = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(.[a-z0-9-]+)*/; + const emailAddr = this.client.user.getProperty(UserProps.EmailAddress); + if(!emailAddr || !emailRegExp.test(emailAddr)) { + const info = this.config.emailRequired || DefaultMsg.emailRequired; + return this.displayDetails(info, cb); + } + const otpTypeProp = this.otpTypeFromOTPTypeIndex(_.get(formData, 'value.otpType')); + const saveFailedError = (err) => { + const info = this.config.saveError || DefaultMsg.saveError; + this.displayDetails(info, () => { + return cb(err); + }); + }; + // sanity check if(!otpFromType(otpTypeProp)) { - return cb(Errors.Invalid('Cannot convert selected index to valid OTP type')); + return saveFailedError(Errors.Invalid('Cannot convert selected index to valid OTP type')); } this.removeUserOTPProperties(err => { if(err) { - return cb(err); + return saveFailedError(err); } - return WebRegister.sendRegisterEmail(this.client.user, otpTypeProp, cb); + WebRegister.sendRegisterEmail(this.client.user, otpTypeProp, err => { + if(err) { + return saveFailedError(err); + } + + const info = this.config.saveEmailSent || DefaultMsg.saveEmailSent; + return this.displayDetails(info, cb); + }); }); } @@ -217,18 +274,18 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { } saveChangesDisable(cb) { - this.removeUserOTPProperties(this.client.user, err => { + this.removeUserOTPProperties(err => { if(err) { return cb(err); } - // :TODO: show "saved+disabled" art/message -> prevMenu - return cb(null); + const info = this.config.saveDisabled || DefaultMsg.saveDisabled; + return this.displayDetails(info, cb); }); } isOTPEnabledForUser() { - return this.otpTypeIndexFromUserOTPType(-1) != -1; + return this.client.user.getProperty(UserProps.AuthFactor2OTP) ? true : false; } getInfoText(key) { diff --git a/core/user_2fa_otp_web_register.js b/core/user_2fa_otp_web_register.js index a708969e..2b7d294c 100644 --- a/core/user_2fa_otp_web_register.js +++ b/core/user_2fa_otp_web_register.js @@ -19,6 +19,9 @@ const { const { sendMail } = require('./email.js'); const UserProps = require('./user_property.js'); const Log = require('./logger.js').log; +const { + getConnectionByUserId +} = require('./client_connections.js'); // deps const async = require('async'); @@ -104,7 +107,7 @@ module.exports = class User2FA_OTPWebRegister if(err) { Log.warn({ error : err.message }, 'Failed sending 2FA/OTP register email'); } else { - Log.info( { info }, 'Successfully sent 2FA/OTP register email'); + Log.info({ info }, 'Successfully sent 2FA/OTP register email'); } return callback(err); }); @@ -172,7 +175,7 @@ module.exports = class User2FA_OTPWebRegister .replace(/%TOKEN%/g, token) .replace(/%OTP_TYPE%/g, otpType) .replace(/%POST_URL%/g, postUrl) - .replace(/%QR_IMG_DATA%/g, otpInfo.qr) + .replace(/%QR_IMG_DATA%/g, otpInfo.qr || '') .replace(/%SECRET%/g, otpInfo.secret) ; return next(null, finalPage); @@ -232,6 +235,17 @@ module.exports = class User2FA_OTPWebRegister return webServer.instance.respondWithError(resp, 500, 'Internal Server Error', 'Internal Server Error'); } + // + // User may be online still - find account & update it if so + // + const clientConn = getConnectionByUserId(tokenInfo.user.userId); + if(clientConn && clientConn.user) { + // just update live props, we've already persisted them. + _.each(props, (v, n) => { + clientConn.user.setProperty(n, v); + }); + } + // we can now remove the token - no need to wait deleteToken(formData.token, err => { if(err) { @@ -261,7 +275,7 @@ ${backupCodes} [ { method : 'GET', - path : '^\\/enable_2fa_otp\\?token\\=[a-f0-9]+&otpType\\=[a-zA-Z0-9]+$', + path : '^\\/enable_2fa_otp\\?token\\=[a-f0-9]+&otpType\\=[a-zA-Z0-9_]+$', handler : User2FA_OTPWebRegister.routeRegisterGet, }, { diff --git a/www/otp_register.template.html b/www/otp_register.template.html index 22d00866..90eeba16 100644 --- a/www/otp_register.template.html +++ b/www/otp_register.template.html @@ -11,10 +11,12 @@ Your OTP secret:
%SECRET%

-

- QR Code:
- -

+
Confirm One-Time-Password to continue: