enigma-bbs/core/user_2fa_otp_web_register.js

314 lines
11 KiB
JavaScript

/* jslint node: true */
'use strict';
// ENiGMA½
const Config = require('./config.js').get;
const getServer = require('./listening_server.js').getServer;
const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName;
const {
createToken,
deleteToken,
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;
const { getConnectionByUserId } = require('./client_connections.js');
// 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(
`/_internal/enable_2fa_otp?token=${token}&otpType=${otpType}`
);
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 !== WellKnownTokenTypes.AuthFactor2OTPRegister) {
return User2FA_OTPWebRegister.accessDenied(webServer, resp);
}
const prepareOptions = {
qrType: 'data',
cellSize: 8,
username: tokenInfo.user.username,
};
prepareOTP(otpType, prepareOptions, (err, otpInfo) => {
if (err) {
Log.error({ error: err.message }, 'Failed to prepare OTP');
return User2FA_OTPWebRegister.accessDenied(webServer, resp);
}
const postUrl = webServer.instance.buildUrl('/_internal/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
);
});
});
}
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);
if (
!formData.token ||
!formData.otpType ||
!formData.otp ||
!formData.secret
) {
return badRequest();
}
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'
);
}
//
// 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
deleteToken(formData.token, err => {
if (err) {
Log.error(
{ error: err.message, token: formData.token },
'Failed to delete temporary 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) {
const webServer = getWebServer();
if (!webServer || !webServer.instance.isEnabled()) {
return cb(null); // no webserver enabled
}
[
{
method: 'GET',
path: /^\/_internal\/enable_2fa_otp\?token=[a-f0-9]+&otpType=[a-zA-Z0-9_]+$/,
handler: User2FA_OTPWebRegister.routeRegisterGet,
},
{
method: 'POST',
path: /^\/_internal\/enable_2fa_otp$/,
handler: User2FA_OTPWebRegister.routeRegisterPost,
},
].forEach(r => {
webServer.instance.addRoute(r);
});
return cb(null);
}
};