From 94747cfe7e813fd31197a217034ec1ea188db218 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 13 Jun 2019 19:47:04 -0600 Subject: [PATCH] Good progress on 2FA/OTP config: Most of email register lifecycle complete --- core/oputil/oputil_user.js | 3 +- core/user_2fa_otp.js | 10 +-- core/user_2fa_otp_web_register.js | 112 ++++++++++++++++++++++-------- www/otp_register.template.html | 42 +++++------ 4 files changed, 107 insertions(+), 60 deletions(-) diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index 7b441875..78ea08ca 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -370,6 +370,7 @@ function twoFactorAuthOTP(user) { const { OTPTypes, prepareOTP, + createBackupCodes, } = require('../../core/user_2fa_otp.js'); let otpType = argv._[argv._.length - 1]; @@ -414,7 +415,7 @@ function twoFactorAuthOTP(user) { qrType : argv['qr-type'] || 'ascii', }; prepareOTP(otpType, otpOpts, (err, otpInfo) => { - return callback(err, Object.assign(otpInfo, { otpType })); + return callback(err, Object.assign(otpInfo, { otpType, backupCodes : createBackupCodes() })); }); }, function storeOrDisplayQR(otpInfo, callback) { diff --git a/core/user_2fa_otp.js b/core/user_2fa_otp.js index 0b53ad8b..6d5ae6e1 100644 --- a/core/user_2fa_otp.js +++ b/core/user_2fa_otp.js @@ -20,6 +20,7 @@ const crypto = require('crypto'); const qrGen = require('qrcode-generator'); exports.prepareOTP = prepareOTP; +exports.createBackupCodes = createBackupCodes; exports.createQRCode = createQRCode; exports.otpFromType = otpFromType; exports.loginFactor2_OTP = loginFactor2_OTP; @@ -82,7 +83,7 @@ function generateOTPBackupCode() { return bits.join('-'); } -function generateNewBackupCodes() { +function createBackupCodes() { const codes = [...Array(6)].map(() => generateOTPBackupCode()); return codes; } @@ -120,7 +121,7 @@ function createQRCode(otp, options, secret) { data : qrCode.createDataURL, img : qrCode.createImgTag, svg : qrCode.createSvgTag, - }[options.qrType](); + }[options.qrType](options.cellSize); } catch(e) { return; } @@ -141,10 +142,9 @@ function prepareOTP(otpType, options, cb) { otp.generateSecret() : crypto.randomBytes(64).toString('base64').substr(0, 32); - const backupCodes = generateNewBackupCodes(); - const qr = createQRCode(otp, options, secret); + const qr = createQRCode(otp, options, secret); - return cb(null, { secret, backupCodes, qr } ); + return cb(null, { secret, qr } ); } function loginFactor2_OTP(client, token, cb) { diff --git a/core/user_2fa_otp_web_register.js b/core/user_2fa_otp_web_register.js index 840a5dd3..52ee96ad 100644 --- a/core/user_2fa_otp_web_register.js +++ b/core/user_2fa_otp_web_register.js @@ -11,6 +11,11 @@ const { getTokenInfo, WellKnownTokenTypes, } = require('./user_temp_token.js'); +const { + prepareOTP, + createBackupCodes, + otpFromType, +} = require('./user_2fa_otp.js'); const { sendMail } = require('./email.js'); const UserProps = require('./user_property.js'); const Log = require('./logger.js').log; @@ -70,7 +75,7 @@ module.exports = class User2FA_OTPWebRegister (token, textTemplate, htmlTemplate, callback) => { const webServer = getWebServer(); const registerUrl = webServer.instance.buildUrl( - `/enable_2fa_otp?token=&otpType=${otpType}&token=${token}` + `/enable_2fa_otp?token=${token}&otpType=${otpType}` ); const replaceTokens = (s) => { @@ -133,36 +138,48 @@ module.exports = class User2FA_OTPWebRegister getTokenInfo(token, (err, tokenInfo) => { if(err) { // assume expired - return webServer.instance.respondWithError(resp, 410, 'Invalid or expired registration link.', 'Expired Link'); + return webServer.instance.respondWithError( + resp, + 410, + 'Invalid or expired registration link.', 'Expired Link' + ); } if(tokenInfo.tokenType !== 'auth_factor2_otp_register') { return User2FA_OTPWebRegister.accessDenied(webServer, resp); } - const qrImg = ''; // :TODO: fix me - const secret = ''; - const backupCodes = ''; + const prepareOptions = { + qrType : 'data', + cellSize : 8, + username : tokenInfo.user.username, + }; - const postUrl = webServer.instance.buildUrl('/enable_2fa_otp'); - const config = Config(); - return webServer.instance.routeTemplateFilePage( - _.get(config, 'users.twoFactorAuth.otp.registerPageTemplate'), - (templateData, next) => { - const finalPage = templateData - .replace(/%BOARDNAME%/g, config.general.boardName) - .replace(/%USERNAME%/g, tokenInfo.user.username) - .replace(/%TOKEN%/g, token) - .replace(/%OTP_TYPE%/g, otpType) - .replace(/%POST_URL%/g, postUrl) - .replace(/%QR_IMG%/g, qrImg) - .replace(/%SECRET%/g, secret) - .replace(/%BACKUP_CODES%/g, backupCodes) - ; - return next(null, finalPage); - }, - resp - ); + prepareOTP(otpType, prepareOptions, (err, otpInfo) => { + if(err) { + // :TODO: Log error + return User2FA_OTPWebRegister.accessDenied(webServer, resp); + } + + const postUrl = webServer.instance.buildUrl('/enable_2fa_otp'); + const config = Config(); + return webServer.instance.routeTemplateFilePage( + _.get(config, 'users.twoFactorAuth.otp.registerPageTemplate'), + (templateData, next) => { + const finalPage = templateData + .replace(/%BOARDNAME%/g, config.general.boardName) + .replace(/%USERNAME%/g, tokenInfo.user.username) + .replace(/%TOKEN%/g, token) + .replace(/%OTP_TYPE%/g, otpType) + .replace(/%POST_URL%/g, postUrl) + .replace(/%QR_IMG_DATA%/g, otpInfo.qr) + .replace(/%SECRET%/g, otpInfo.secret) + ; + return next(null, finalPage); + }, + resp + ); + }); }); } @@ -181,13 +198,52 @@ module.exports = class User2FA_OTPWebRegister req.on('end', () => { const formData = querystring.parse(bodyData); - const config = Config(); - if(!formData.token || !formData.otpType || !formData.otp) { + if(!formData.token || !formData.otpType || !formData.otp || + !formData.secret) + { return badRequest(); } - }); - return webServer.instance.respondWithError(resp, 410, 'Invalid or expired registration link.', 'Expired Link'); + const otp = otpFromType(formData.otpType); + if(!otp) { + return badRequest(); + } + + const valid = otp.verify( { token : formData.otp, secret : formData.secret } ); + if(!valid) { + return User2FA_OTPWebRegister.accessDenied(webServer, resp); + } + + getTokenInfo(formData.token, (err, tokenInfo) => { + if(err) { + return User2FA_OTPWebRegister.accessDenied(webServer, resp); + } + + const backupCodes = createBackupCodes(); + + const props = { + [ UserProps.AuthFactor2OTP ] : formData.otpType, + [ UserProps.AuthFactor2OTPSecret ] : formData.secret, + [ UserProps.AuthFactor2OTPBackupCodes ] : JSON.stringify(backupCodes), + }; + + tokenInfo.user.persistProperties(props, err => { + if(err) { + return webServer.instance.respondWithError(resp, 500, 'Internal Server Error', 'Internal Server Error'); + } + // :TODO: remove token + + // :TODO: use a html template here too, if provided + resp.writeHead(200); + return resp.end( + `2-Factor Authentication via One-Time-Password has been enabled for this account. Please write down your backup codes and store them in safe place: + +${backupCodes} + ` + ); + }); + }); + }); } static registerRoutes(cb) { diff --git a/www/otp_register.template.html b/www/otp_register.template.html index a743928b..22d00866 100644 --- a/www/otp_register.template.html +++ b/www/otp_register.template.html @@ -5,33 +5,23 @@ Enable 2FA/OTP — ENiGMA½ BBS - -
- Enable One-Time-Password - - - - -
+

+ Your OTP secret:
+ %SECRET% +

+

+ QR Code:
+ +

+
+ Confirm One-Time-Password to continue: + + + + + +
\ No newline at end of file