Work on 2FA/OTP email system
* Web routes/handler/etc. mostly functional * Can now enable -> follow link -> submit -> capture form * Clean up code
This commit is contained in:
parent
fa3e3e5802
commit
3efea3de9a
|
@ -306,6 +306,10 @@ function initialize(cb) {
|
||||||
const WebPasswordReset = require('./web_password_reset.js').WebPasswordReset;
|
const WebPasswordReset = require('./web_password_reset.js').WebPasswordReset;
|
||||||
return WebPasswordReset.startup(callback);
|
return WebPasswordReset.startup(callback);
|
||||||
},
|
},
|
||||||
|
function ready2FA_OTPRegister(callback) {
|
||||||
|
const User2FA_OTPWebRegister = require('./user_2fa_otp_web_register.js');
|
||||||
|
return User2FA_OTPWebRegister.startup(callback);
|
||||||
|
},
|
||||||
function readyEventScheduler(callback) {
|
function readyEventScheduler(callback) {
|
||||||
const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule;
|
const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule;
|
||||||
EventSchedulerModule.loadAndStart( (err, modInst) => {
|
EventSchedulerModule.loadAndStart( (err, modInst) => {
|
||||||
|
|
|
@ -235,8 +235,9 @@ function getDefaultConfig() {
|
||||||
method : 'googleAuth',
|
method : 'googleAuth',
|
||||||
|
|
||||||
otp : {
|
otp : {
|
||||||
registerEmailText : paths.join(__dirname, '../misc/otp_register_email.template.txt'),
|
registerEmailText : paths.join(__dirname, '../misc/otp_register_email.template.txt'),
|
||||||
registerEmailHtml : paths.join(__dirname, '../misc/otp_register_email.template.html')
|
registerEmailHtml : paths.join(__dirname, '../misc/otp_register_email.template.html'),
|
||||||
|
registerPageTemplate : paths.join(__dirname, '../www/otp_register.template.html'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,7 +15,7 @@ exports.sendMail = sendMail;
|
||||||
function sendMail(message, cb) {
|
function sendMail(message, cb) {
|
||||||
const config = Config();
|
const config = Config();
|
||||||
if(!_.has(config, 'email.transport')) {
|
if(!_.has(config, 'email.transport')) {
|
||||||
return cb(Errors.MissingConfig('Email "email::transport" configuration missing'));
|
return cb(Errors.MissingConfig('Email "email.transport" configuration missing'));
|
||||||
}
|
}
|
||||||
|
|
||||||
message.from = message.from || config.email.defaultFrom;
|
message.from = message.from || config.email.defaultFrom;
|
||||||
|
|
|
@ -10,20 +10,15 @@ const {
|
||||||
createQRCode,
|
createQRCode,
|
||||||
} = require('./user_2fa_otp.js');
|
} = require('./user_2fa_otp.js');
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
const { sendMail } = require('./email.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 {
|
|
||||||
createToken,
|
|
||||||
WellKnownTokenTypes,
|
|
||||||
} = require('./user_temp_token.js');
|
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
|
const WebRegister = require('./user_2fa_otp_web_register.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require('iconv-lite');
|
||||||
const fs = require('fs-extra');
|
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'User 2FA/OTP Configuration',
|
name : 'User 2FA/OTP Configuration',
|
||||||
|
@ -49,17 +44,6 @@ const DefaultMsg = {
|
||||||
noBackupCodes : 'No backup codes remaining or set.',
|
noBackupCodes : 'No backup codes remaining or set.',
|
||||||
};
|
};
|
||||||
|
|
||||||
const DefaultEmailTextTemplate =
|
|
||||||
`%USERNAME%:
|
|
||||||
You have requested to enable 2-Factor Authentication via One-Time-Password
|
|
||||||
for your account on %BOARDNAME%.
|
|
||||||
|
|
||||||
* If this was not you, please ignore this email and change your password.
|
|
||||||
* Otherwise, please follow the link below:
|
|
||||||
|
|
||||||
%REGISTER_URL%
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
|
exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
@ -82,8 +66,8 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
initSequence() {
|
initSequence() {
|
||||||
this.webServer = getServer(WebServerPackageName);
|
const webServer = getServer(WebServerPackageName);
|
||||||
if(!this.webServer || !this.webServer.instance.isEnabled()) {
|
if(!webServer || !webServer.instance.isEnabled()) {
|
||||||
this.client.log.warn('User 2FA/OTP configuration requires the web server to be enabled!');
|
this.client.log.warn('User 2FA/OTP configuration requires the web server to be enabled!');
|
||||||
return this.prevMenu( () => { /* dummy */ } );
|
return this.prevMenu( () => { /* dummy */ } );
|
||||||
}
|
}
|
||||||
|
@ -215,66 +199,12 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
|
||||||
return cb(Errors.Invalid('Cannot convert selected index to valid OTP type'));
|
return cb(Errors.Invalid('Cannot convert selected index to valid OTP type'));
|
||||||
}
|
}
|
||||||
|
|
||||||
async.waterfall(
|
this.removeUserOTPProperties(err => {
|
||||||
[
|
if(err) {
|
||||||
(callback) => {
|
|
||||||
return this.removeUserOTPProperties(callback);
|
|
||||||
},
|
|
||||||
(callback) => {
|
|
||||||
return createToken(this.client.user.userId, WellKnownTokenTypes.AuthFactor2OTPRegister, callback);
|
|
||||||
},
|
|
||||||
(token, callback) => {
|
|
||||||
const config = Config();
|
|
||||||
const txtTemplateFile = _.get(config, 'users.twoFactorAuth.otp.registerEmailText');
|
|
||||||
const htmlTemplateFile = _.get(config, 'users.twoFactorAuth.otp.registerEmailHtml');
|
|
||||||
|
|
||||||
fs.readFile(txtTemplateFile, 'utf8', (err, textTemplate) => {
|
|
||||||
textTemplate = textTemplate || DefaultEmailTextTemplate;
|
|
||||||
fs.readFile(htmlTemplateFile, 'utf8', (err, htmlTemplate) => {
|
|
||||||
htmlTemplate = htmlTemplate || null; // be explicit for waterfall
|
|
||||||
return callback(null, token, textTemplate, htmlTemplate);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
(token, textTemplate, htmlTemplate, callback) => {
|
|
||||||
const registerUrl = this.webServer.instance.buildUrl(
|
|
||||||
`/enable_2fa_otp?token=&otpType=${otpTypeProp}&token=${token}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const user = this.client.user;
|
|
||||||
|
|
||||||
const replaceTokens = (s) => {
|
|
||||||
return s
|
|
||||||
.replace(/%BOARDNAME%/g, Config().general.boardName)
|
|
||||||
.replace(/%USERNAME%/g, user.username)
|
|
||||||
.replace(/%TOKEN%/g, token)
|
|
||||||
.replace(/%REGISTER_URL%/g, registerUrl)
|
|
||||||
;
|
|
||||||
};
|
|
||||||
|
|
||||||
textTemplate = replaceTokens(textTemplate);
|
|
||||||
if(htmlTemplate) {
|
|
||||||
htmlTemplate = replaceTokens(htmlTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = {
|
|
||||||
to : `${user.getProperty(UserProps.RealName) || user.username} <${user.getProperty(UserProps.EmailAddress)}>`,
|
|
||||||
// from will be filled in
|
|
||||||
subject : '2-Factor Authentication Registration',
|
|
||||||
text : textTemplate,
|
|
||||||
html : htmlTemplate,
|
|
||||||
};
|
|
||||||
|
|
||||||
sendMail(message, (err, info) => {
|
|
||||||
// :TODO: Log info!
|
|
||||||
return callback(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
],
|
|
||||||
err => {
|
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
);
|
return WebRegister.sendRegisterEmail(this.client.user, otpTypeProp, cb);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
removeUserOTPProperties(cb) {
|
removeUserOTPProperties(cb) {
|
||||||
|
@ -287,7 +217,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
saveChangesDisable(cb) {
|
saveChangesDisable(cb) {
|
||||||
this.removeUserOTPProperties( err => {
|
this.removeUserOTPProperties(this.client.user, err => {
|
||||||
if(err) {
|
if(err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,216 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ENiGMA½
|
||||||
|
const Config = require('./config.js').get;
|
||||||
|
const Errors = require('./enig_error.js').Errors;
|
||||||
|
const getServer = require('./listening_server.js').getServer;
|
||||||
|
const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName;
|
||||||
|
const {
|
||||||
|
createToken,
|
||||||
|
getTokenInfo,
|
||||||
|
WellKnownTokenTypes,
|
||||||
|
} = require('./user_temp_token.js');
|
||||||
|
const { sendMail } = require('./email.js');
|
||||||
|
const UserProps = require('./user_property.js');
|
||||||
|
const Log = require('./logger.js').log;
|
||||||
|
|
||||||
|
// deps
|
||||||
|
const async = require('async');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const url = require('url');
|
||||||
|
const querystring = require('querystring');
|
||||||
|
|
||||||
|
function getWebServer() {
|
||||||
|
return getServer(webServerPackageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultEmailTextTemplate =
|
||||||
|
`%USERNAME%:
|
||||||
|
You have requested to enable 2-Factor Authentication via One-Time-Password
|
||||||
|
for your account on %BOARDNAME%.
|
||||||
|
|
||||||
|
* If this was not you, please ignore this email and change your password.
|
||||||
|
* Otherwise, please follow the link below:
|
||||||
|
|
||||||
|
%REGISTER_URL%
|
||||||
|
`;
|
||||||
|
|
||||||
|
module.exports = class User2FA_OTPWebRegister
|
||||||
|
{
|
||||||
|
static startup(cb) {
|
||||||
|
return User2FA_OTPWebRegister.registerRoutes(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
static sendRegisterEmail(user, otpType, cb) {
|
||||||
|
async.waterfall(
|
||||||
|
[
|
||||||
|
(callback) => {
|
||||||
|
return createToken(
|
||||||
|
user.userId,
|
||||||
|
WellKnownTokenTypes.AuthFactor2OTPRegister,
|
||||||
|
{ bits : 128 },
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(token, callback) => {
|
||||||
|
const config = Config();
|
||||||
|
const txtTemplateFile = _.get(config, 'users.twoFactorAuth.otp.registerEmailText');
|
||||||
|
const htmlTemplateFile = _.get(config, 'users.twoFactorAuth.otp.registerEmailHtml');
|
||||||
|
|
||||||
|
fs.readFile(txtTemplateFile, 'utf8', (err, textTemplate) => {
|
||||||
|
textTemplate = textTemplate || DefaultEmailTextTemplate;
|
||||||
|
fs.readFile(htmlTemplateFile, 'utf8', (err, htmlTemplate) => {
|
||||||
|
htmlTemplate = htmlTemplate || null; // be explicit for waterfall
|
||||||
|
return callback(null, token, textTemplate, htmlTemplate);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(token, textTemplate, htmlTemplate, callback) => {
|
||||||
|
const webServer = getWebServer();
|
||||||
|
const registerUrl = webServer.instance.buildUrl(
|
||||||
|
`/enable_2fa_otp?token=&otpType=${otpType}&token=${token}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const replaceTokens = (s) => {
|
||||||
|
return s
|
||||||
|
.replace(/%BOARDNAME%/g, Config().general.boardName)
|
||||||
|
.replace(/%USERNAME%/g, user.username)
|
||||||
|
.replace(/%TOKEN%/g, token)
|
||||||
|
.replace(/%REGISTER_URL%/g, registerUrl)
|
||||||
|
;
|
||||||
|
};
|
||||||
|
|
||||||
|
textTemplate = replaceTokens(textTemplate);
|
||||||
|
if(htmlTemplate) {
|
||||||
|
htmlTemplate = replaceTokens(htmlTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
to : `${user.getProperty(UserProps.RealName) || user.username} <${user.getProperty(UserProps.EmailAddress)}>`,
|
||||||
|
// from will be filled in
|
||||||
|
subject : '2-Factor Authentication Registration',
|
||||||
|
text : textTemplate,
|
||||||
|
html : htmlTemplate,
|
||||||
|
};
|
||||||
|
|
||||||
|
sendMail(message, (err, info) => {
|
||||||
|
if(err) {
|
||||||
|
Log.warn({ error : err.message }, 'Failed sending 2FA/OTP register email');
|
||||||
|
} else {
|
||||||
|
Log.info( { info }, 'Successfully sent 2FA/OTP register email');
|
||||||
|
}
|
||||||
|
return callback(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fileNotFound(webServer, resp) {
|
||||||
|
return webServer.instance.fileNotFound(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
static accessDenied(webServer, resp) {
|
||||||
|
return webServer.instance.accessDenied(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
static routeRegisterGet(req, resp) {
|
||||||
|
const webServer = getWebServer(); // must be valid, we just got a req!
|
||||||
|
|
||||||
|
const urlParts = url.parse(req.url, true);
|
||||||
|
const token = urlParts.query && urlParts.query.token;
|
||||||
|
const otpType = urlParts.query && urlParts.query.otpType;
|
||||||
|
|
||||||
|
if(!token || !otpType) {
|
||||||
|
return User2FA_OTPWebRegister.accessDenied(webServer, resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTokenInfo(token, (err, tokenInfo) => {
|
||||||
|
if(err) {
|
||||||
|
// assume expired
|
||||||
|
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 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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static routeRegisterPost(req, resp) {
|
||||||
|
const webServer = getWebServer(); // must be valid, we just got a req!
|
||||||
|
|
||||||
|
const badRequest = () => {
|
||||||
|
return webServer.instance.respondWithError(resp, 400, 'Bad Request.', 'Bad Request');
|
||||||
|
};
|
||||||
|
|
||||||
|
let bodyData = '';
|
||||||
|
req.on('data', data => {
|
||||||
|
bodyData += data;
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('end', () => {
|
||||||
|
const formData = querystring.parse(bodyData);
|
||||||
|
|
||||||
|
const config = Config();
|
||||||
|
if(!formData.token || !formData.otpType || !formData.otp) {
|
||||||
|
return badRequest();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return webServer.instance.respondWithError(resp, 410, 'Invalid or expired registration link.', 'Expired Link');
|
||||||
|
}
|
||||||
|
|
||||||
|
static registerRoutes(cb) {
|
||||||
|
const webServer = getWebServer();
|
||||||
|
if(!webServer || !webServer.instance.isEnabled()) {
|
||||||
|
return cb(null); // no webserver enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
method : 'GET',
|
||||||
|
path : '^\\/enable_2fa_otp\\?token\\=[a-f0-9]+&otpType\\=[a-zA-Z0-9]+$',
|
||||||
|
handler : User2FA_OTPWebRegister.routeRegisterGet,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method : 'POST',
|
||||||
|
path : '^\\/enable_2fa_otp$',
|
||||||
|
handler : User2FA_OTPWebRegister.routeRegisterPost,
|
||||||
|
}
|
||||||
|
].forEach(r => {
|
||||||
|
webServer.instance.addRoute(r);
|
||||||
|
});
|
||||||
|
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
|
};
|
|
@ -25,17 +25,17 @@ exports.WellKnownTokenTypes = {
|
||||||
AuthFactor2OTPRegister : 'auth_factor2_otp_register',
|
AuthFactor2OTPRegister : 'auth_factor2_otp_register',
|
||||||
};
|
};
|
||||||
|
|
||||||
function createToken(userId, tokenType, cb) {
|
function createToken(userId, tokenType, options = { bits : 128 }, cb) {
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
(callback) => {
|
(callback) => {
|
||||||
return crypto.randomBytes(256, callback);
|
return crypto.randomBytes(options.bits, callback);
|
||||||
},
|
},
|
||||||
(token, callback) => {
|
(token, callback) => {
|
||||||
token = token.toString('hex');
|
token = token.toString('hex');
|
||||||
|
|
||||||
UserDb.run(
|
UserDb.run(
|
||||||
`INSERT INTO user_temporary_token (user_id, token, token_type, timestamp)
|
`INSERT OR REPLACE INTO user_temporary_token (user_id, token, token_type, timestamp)
|
||||||
VALUES (?, ?, ?, ?);`,
|
VALUES (?, ?, ?, ?);`,
|
||||||
[ userId, token, tokenType, getISOTimestampString() ],
|
[ userId, token, tokenType, getISOTimestampString() ],
|
||||||
err => {
|
err => {
|
||||||
|
|
|
@ -133,7 +133,7 @@ class WebPasswordReset {
|
||||||
if(err) {
|
if(err) {
|
||||||
Log.warn( { error : err.message }, 'Failed sending password reset email' );
|
Log.warn( { error : err.message }, 'Failed sending password reset email' );
|
||||||
} else {
|
} else {
|
||||||
Log.debug( { info : info }, 'Successfully sent password reset email');
|
Log.info( { info : info }, 'Successfully sent password reset email');
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback(err);
|
return callback(err);
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Enable 2FA/OTP — ENiGMA½ BBS</title>
|
||||||
|
<meta name="description" content="Enable 2-Factor Authentication via One-Time-Password">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<script>
|
||||||
|
window.onload = function() {
|
||||||
|
//document.getElementById('password').onchange = validatePassword;
|
||||||
|
//document.getElementById('confirm_password').onchange = validatePassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
function validatePassword() {
|
||||||
|
var pw = document.getElementById('password');
|
||||||
|
var confirm = document.getElementById('confirm_password');
|
||||||
|
|
||||||
|
if(pw.value !== confirm.value) {
|
||||||
|
confirm.setCustomValidity('Passwords must match!');
|
||||||
|
} else {
|
||||||
|
confirm.setCustomValidity('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<form action="%POST_URL%" method="post">
|
||||||
|
<legend>Enable One-Time-Password</legend>
|
||||||
|
<input type="text" placeholder="One Time Password" id="otp" name="otp" required>
|
||||||
|
<input type="hidden" value="%TOKEN%" name="token">
|
||||||
|
<input type="hidden" value="%OTP_TYPE%" name="otpType">
|
||||||
|
<button type="submit">Confirm</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue