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:
-
-
+