Introduce new user_temporary_token & use for token storage of 2FA/OTP registration
This commit is contained in:
parent
18eecb6223
commit
fa3e3e5802
|
@ -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')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -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%
|
||||||
|
|
Loading…
Reference in New Issue