Introduce new user_temporary_token & use for token storage of 2FA/OTP registration

This commit is contained in:
Bryan Ashby 2019-06-11 21:20:34 -06:00
parent 18eecb6223
commit fa3e3e5802
No known key found for this signature in database
GPG Key ID: B49EB437951D2542
5 changed files with 203 additions and 14 deletions

View File

@ -233,6 +233,11 @@ function getDefaultConfig() {
twoFactorAuth : { twoFactorAuth : {
method : 'googleAuth', method : 'googleAuth',
otp : {
registerEmailText : paths.join(__dirname, '../misc/otp_register_email.template.txt'),
registerEmailHtml : paths.join(__dirname, '../misc/otp_register_email.template.html')
}
} }
}, },

View File

@ -13,12 +13,17 @@ const { Errors } = require('./enig_error.js');
const { sendMail } = require('./email.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;
// 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 crypto = require('crypto'); const fs = require('fs-extra');
exports.moduleInfo = { exports.moduleInfo = {
name : 'User 2FA/OTP Configuration', name : 'User 2FA/OTP Configuration',
@ -44,6 +49,17 @@ 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);
@ -205,25 +221,54 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule {
return this.removeUserOTPProperties(callback); return this.removeUserOTPProperties(callback);
}, },
(callback) => { (callback) => {
return crypto.randomBytes(256, callback); return createToken(this.client.user.userId, WellKnownTokenTypes.AuthFactor2OTPRegister, callback);
}, },
(token, callback) => { (token, callback) => {
// :TODO: consider temporary tokens table - this has become semi-common const config = Config();
// token | timestamp | token_type | const txtTemplateFile = _.get(config, 'users.twoFactorAuth.otp.registerEmailText');
// abc | ISO | '2fa_otp_register' const htmlTemplateFile = _.get(config, 'users.twoFactorAuth.otp.registerEmailHtml');
token = token.toString('hex');
this.client.user.persistProperty(UserProps.AuthFactor2OTPEnableToken, token, err => { fs.readFile(txtTemplateFile, 'utf8', (err, textTemplate) => {
return callback(err, token); textTemplate = textTemplate || DefaultEmailTextTemplate;
fs.readFile(htmlTemplateFile, 'utf8', (err, htmlTemplate) => {
htmlTemplate = htmlTemplate || null; // be explicit for waterfall
return callback(null, token, textTemplate, htmlTemplate);
});
}); });
}, },
(token, callback) => { (token, textTemplate, htmlTemplate, callback) => {
const resetUrl = this.webServer.instance.buildUrl( const registerUrl = this.webServer.instance.buildUrl(
`/enable_2fa_otp?token=&otpType=${otpTypeProp}&token=${token}` `/enable_2fa_otp?token=&otpType=${otpTypeProp}&token=${token}`
); );
// clear any existing (e.g. same as disable) -> send activation email const user = this.client.user;
return callback(null); 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 => { err => {

View File

@ -63,6 +63,5 @@ module.exports = {
AuthFactor2OTP : 'auth_factor2_otp', // If present, OTP type for 2FA. See OTPTypes AuthFactor2OTP : 'auth_factor2_otp', // If present, OTP type for 2FA. See OTPTypes
AuthFactor2OTPSecret : 'auth_factor2_otp_secret', // Secret used in conjunction with OTP 2FA AuthFactor2OTPSecret : 'auth_factor2_otp_secret', // Secret used in conjunction with OTP 2FA
AuthFactor2OTPBackupCodes : 'auth_factor2_otp_backup', // JSON array of backup codes AuthFactor2OTPBackupCodes : 'auth_factor2_otp_backup', // JSON array of backup codes
AuthFactor2OTPEnableToken : 'auth_factor2_otp_enable_token',
}; };

140
core/user_temp_token.js Normal file
View File

@ -0,0 +1,140 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const UserDb = require('./database.js').dbs.user;
const {
getISOTimestampString
} = require('./database.js');
const { Errors } = require('./enig_error.js');
const User = require('./user.js');
const Log = require('./logger.js').log;
// deps
const crypto = require('crypto');
const async = require('async');
const moment = require('moment');
exports.createToken = createToken;
exports.deleteToken = deleteToken;
exports.deleteTokenByUserAndType = deleteTokenByUserAndType;
exports.getTokenInfo = getTokenInfo;
exports.temporaryTokenMaintenanceTask = temporaryTokenMaintenanceTask;
exports.WellKnownTokenTypes = {
AuthFactor2OTPRegister : 'auth_factor2_otp_register',
};
function createToken(userId, tokenType, cb) {
async.waterfall(
[
(callback) => {
return crypto.randomBytes(256, callback);
},
(token, callback) => {
token = token.toString('hex');
UserDb.run(
`INSERT INTO user_temporary_token (user_id, token, token_type, timestamp)
VALUES (?, ?, ?, ?);`,
[ userId, token, tokenType, getISOTimestampString() ],
err => {
return callback(err, token);
}
);
}
],
(err, token) => {
return cb(err, token);
}
);
}
function deleteToken(token, cb) {
UserDb.run(
`DELETE FROM user_temporary_token
WHERE token = ?;`,
[ token ],
err => {
return cb(err);
}
);
}
function deleteTokenByUserAndType(userId, tokenType, cb) {
UserDb.run(
`DELETE FROM user_temporary_token
WHERE user_id = ? AND token_type = ?;`,
[ userId, tokenType ],
err => {
return cb(err);
}
);
}
function getTokenInfo(token, cb) {
async.waterfall(
[
(callback) => {
UserDb.get(
`SELECT user_id, token_type, timestamp
FROM user_temporary_token
WHERE token = ?;`,
[ token ],
(err, row) => {
if(err) {
return callback(err);
}
if(!row) {
return callback(Errors.DoesNotExist('No entry found for token'));
}
const info = {
userId : row.user_id,
tokenType : row.token_type,
timestamp : moment(row.timestamp),
};
return callback(null, info);
}
);
},
(info, callback) => {
User.getUser(info.userId, (err, user) => {
info.user = user;
return callback(err, info);
});
}
],
(err, info) => {
return cb(err, info);
}
);
}
function temporaryTokenMaintenanceTask(args, cb) {
const tokenType = args[0];
if(!tokenType) {
return Log.error('Cannot run temporary token maintenance task with out specifying "tokenType" as argument 0');
}
const expTime = args[1] || '24 hours';
UserDb.run(
`DELETE FROM user_temporary_token
WHERE token IN (
SELECT token
FROM user_temporary_token
WHERE token_type = ?
AND DATETIME("now") >= DATETIME(timestamp, "+${expTime}")
);`,
[ tokenType ],
err => {
if(err) {
Log.warn( { error : err.message, tokenType }, 'Failed deleting user temporary token');
}
return cb(err);
}
);
}

View File

@ -22,7 +22,7 @@ const _ = require('lodash');
const PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT = const PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT =
`%USERNAME%: `%USERNAME%:
a password reset has been requested for your account on %BOARDNAME%. A password reset has been requested for your account on %BOARDNAME%.
* If this was not you, please ignore this email. * If this was not you, please ignore this email.
* Otherwise, follow this link: %RESET_URL% * Otherwise, follow this link: %RESET_URL%