More OTP updates/fixes

* Ensure email address at save
* Fix QR display issues for non-Google Auth
* More cleanup/etc.
This commit is contained in:
Bryan Ashby 2019-06-13 22:54:56 -06:00
parent 7481421898
commit 2b154800c0
No known key found for this signature in database
GPG Key ID: B49EB437951D2542
4 changed files with 90 additions and 17 deletions

View File

@ -123,7 +123,7 @@ function createQRCode(otp, options, secret) {
svg : qrCode.createSvgTag, svg : qrCode.createSvgTag,
}[options.qrType](options.cellSize); }[options.qrType](options.cellSize);
} catch(e) { } catch(e) {
return; return '';
} }
} }

View File

@ -8,11 +8,11 @@ const {
OTPTypes, OTPTypes,
otpFromType, otpFromType,
createQRCode, createQRCode,
createBackupCodes,
} = require('./user_2fa_otp.js'); } = require('./user_2fa_otp.js');
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const { getServer } = require('./listening_server.js'); const { getServer } = require('./listening_server.js');
const WebServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; const WebServerPackageName = require('./servers/content/web.js').moduleInfo.packageName;
const Config = require('./config.js').get;
const WebRegister = require('./user_2fa_otp_web_register.js'); const WebRegister = require('./user_2fa_otp_web_register.js');
// deps // deps
@ -42,6 +42,11 @@ const MciViewIds = {
const DefaultMsg = { const DefaultMsg = {
otpNotEnabled : '2FA/OTP is not currently enabled for this account.', otpNotEnabled : '2FA/OTP is not currently enabled for this account.',
noBackupCodes : 'No backup codes remaining or set.', 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 { exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
@ -59,6 +64,9 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
showBackupCodes : (formData, extraArgs, cb) => { showBackupCodes : (formData, extraArgs, cb) => {
return this.showBackupCodes(cb); return this.showBackupCodes(cb);
}, },
generateNewBackupCodes : (formData, extraArgs, cb) => {
return this.generateNewBackupCodes(cb);
},
saveChanges : (formData, extraArgs, cb) => { saveChanges : (formData, extraArgs, cb) => {
return this.saveChanges(formData, cb); return this.saveChanges(formData, cb);
} }
@ -153,11 +161,18 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
username : this.client.user.username, username : this.client.user.username,
qrType : 'ascii', qrType : 'ascii',
}; };
qrCode = createQRCode( qrCode = createQRCode(
otp, otp,
qrOptions, qrOptions,
this.client.user.getProperty(UserProps.AuthFactor2OTPSecret) 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); return this.displayDetails(qrCode, cb);
@ -186,24 +201,66 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
return this.displayDetails(info, cb); 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) { saveChanges(formData, cb) {
const enabled = 1 === _.get(formData, 'value.enableToggle', 0); const enabled = 1 === _.get(formData, 'value.enableToggle', 0);
return enabled ? this.saveChangesEnable(formData, cb) : this.saveChangesDisable(cb); return enabled ? this.saveChangesEnable(formData, cb) : this.saveChangesDisable(cb);
} }
saveChangesEnable(formData, 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 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 // sanity check
if(!otpFromType(otpTypeProp)) { 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 => { this.removeUserOTPProperties(err => {
if(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) { saveChangesDisable(cb) {
this.removeUserOTPProperties(this.client.user, err => { this.removeUserOTPProperties(err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
// :TODO: show "saved+disabled" art/message -> prevMenu const info = this.config.saveDisabled || DefaultMsg.saveDisabled;
return cb(null); return this.displayDetails(info, cb);
}); });
} }
isOTPEnabledForUser() { isOTPEnabledForUser() {
return this.otpTypeIndexFromUserOTPType(-1) != -1; return this.client.user.getProperty(UserProps.AuthFactor2OTP) ? true : false;
} }
getInfoText(key) { getInfoText(key) {

View File

@ -19,6 +19,9 @@ const {
const { sendMail } = require('./email.js'); const { sendMail } = require('./email.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const {
getConnectionByUserId
} = require('./client_connections.js');
// deps // deps
const async = require('async'); const async = require('async');
@ -104,7 +107,7 @@ module.exports = class User2FA_OTPWebRegister
if(err) { if(err) {
Log.warn({ error : err.message }, 'Failed sending 2FA/OTP register email'); Log.warn({ error : err.message }, 'Failed sending 2FA/OTP register email');
} else { } else {
Log.info( { info }, 'Successfully sent 2FA/OTP register email'); Log.info({ info }, 'Successfully sent 2FA/OTP register email');
} }
return callback(err); return callback(err);
}); });
@ -172,7 +175,7 @@ module.exports = class User2FA_OTPWebRegister
.replace(/%TOKEN%/g, token) .replace(/%TOKEN%/g, token)
.replace(/%OTP_TYPE%/g, otpType) .replace(/%OTP_TYPE%/g, otpType)
.replace(/%POST_URL%/g, postUrl) .replace(/%POST_URL%/g, postUrl)
.replace(/%QR_IMG_DATA%/g, otpInfo.qr) .replace(/%QR_IMG_DATA%/g, otpInfo.qr || '')
.replace(/%SECRET%/g, otpInfo.secret) .replace(/%SECRET%/g, otpInfo.secret)
; ;
return next(null, finalPage); 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'); 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 // we can now remove the token - no need to wait
deleteToken(formData.token, err => { deleteToken(formData.token, err => {
if(err) { if(err) {
@ -261,7 +275,7 @@ ${backupCodes}
[ [
{ {
method : 'GET', 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, handler : User2FA_OTPWebRegister.routeRegisterGet,
}, },
{ {

View File

@ -11,10 +11,12 @@
Your OTP secret:<br> Your OTP secret:<br>
<b>%SECRET%</b> <b>%SECRET%</b>
</p> </p>
<p> <script>
QR Code:<br> if('googleAuth' == '%OTP_TYPE%') {
<img src="%QR_IMG_DATA%"/> document.write('QR Code:<br>');
</p> document.write('<img src="%QR_IMG_DATA%"/>');
}
</script>
<form action="%POST_URL%" method="post"> <form action="%POST_URL%" method="post">
<legend>Confirm One-Time-Password to continue:</legend> <legend>Confirm One-Time-Password to continue:</legend>
<input type="text" placeholder="One Time Password" id="otp" name="otp" required> <input type="text" placeholder="One Time Password" id="otp" name="otp" required>