Good progress on 2FA/OTP config: Most of email register lifecycle complete
This commit is contained in:
parent
3efea3de9a
commit
94747cfe7e
|
@ -370,6 +370,7 @@ function twoFactorAuthOTP(user) {
|
||||||
const {
|
const {
|
||||||
OTPTypes,
|
OTPTypes,
|
||||||
prepareOTP,
|
prepareOTP,
|
||||||
|
createBackupCodes,
|
||||||
} = require('../../core/user_2fa_otp.js');
|
} = require('../../core/user_2fa_otp.js');
|
||||||
|
|
||||||
let otpType = argv._[argv._.length - 1];
|
let otpType = argv._[argv._.length - 1];
|
||||||
|
@ -414,7 +415,7 @@ function twoFactorAuthOTP(user) {
|
||||||
qrType : argv['qr-type'] || 'ascii',
|
qrType : argv['qr-type'] || 'ascii',
|
||||||
};
|
};
|
||||||
prepareOTP(otpType, otpOpts, (err, otpInfo) => {
|
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) {
|
function storeOrDisplayQR(otpInfo, callback) {
|
||||||
|
|
|
@ -20,6 +20,7 @@ const crypto = require('crypto');
|
||||||
const qrGen = require('qrcode-generator');
|
const qrGen = require('qrcode-generator');
|
||||||
|
|
||||||
exports.prepareOTP = prepareOTP;
|
exports.prepareOTP = prepareOTP;
|
||||||
|
exports.createBackupCodes = createBackupCodes;
|
||||||
exports.createQRCode = createQRCode;
|
exports.createQRCode = createQRCode;
|
||||||
exports.otpFromType = otpFromType;
|
exports.otpFromType = otpFromType;
|
||||||
exports.loginFactor2_OTP = loginFactor2_OTP;
|
exports.loginFactor2_OTP = loginFactor2_OTP;
|
||||||
|
@ -82,7 +83,7 @@ function generateOTPBackupCode() {
|
||||||
return bits.join('-');
|
return bits.join('-');
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateNewBackupCodes() {
|
function createBackupCodes() {
|
||||||
const codes = [...Array(6)].map(() => generateOTPBackupCode());
|
const codes = [...Array(6)].map(() => generateOTPBackupCode());
|
||||||
return codes;
|
return codes;
|
||||||
}
|
}
|
||||||
|
@ -120,7 +121,7 @@ function createQRCode(otp, options, secret) {
|
||||||
data : qrCode.createDataURL,
|
data : qrCode.createDataURL,
|
||||||
img : qrCode.createImgTag,
|
img : qrCode.createImgTag,
|
||||||
svg : qrCode.createSvgTag,
|
svg : qrCode.createSvgTag,
|
||||||
}[options.qrType]();
|
}[options.qrType](options.cellSize);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -141,10 +142,9 @@ function prepareOTP(otpType, options, cb) {
|
||||||
otp.generateSecret() :
|
otp.generateSecret() :
|
||||||
crypto.randomBytes(64).toString('base64').substr(0, 32);
|
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) {
|
function loginFactor2_OTP(client, token, cb) {
|
||||||
|
|
|
@ -11,6 +11,11 @@ const {
|
||||||
getTokenInfo,
|
getTokenInfo,
|
||||||
WellKnownTokenTypes,
|
WellKnownTokenTypes,
|
||||||
} = require('./user_temp_token.js');
|
} = require('./user_temp_token.js');
|
||||||
|
const {
|
||||||
|
prepareOTP,
|
||||||
|
createBackupCodes,
|
||||||
|
otpFromType,
|
||||||
|
} = require('./user_2fa_otp.js');
|
||||||
const { sendMail } = require('./email.js');
|
const { sendMail } = require('./email.js');
|
||||||
const UserProps = require('./user_property.js');
|
const UserProps = require('./user_property.js');
|
||||||
const Log = require('./logger.js').log;
|
const Log = require('./logger.js').log;
|
||||||
|
@ -70,7 +75,7 @@ module.exports = class User2FA_OTPWebRegister
|
||||||
(token, textTemplate, htmlTemplate, callback) => {
|
(token, textTemplate, htmlTemplate, callback) => {
|
||||||
const webServer = getWebServer();
|
const webServer = getWebServer();
|
||||||
const registerUrl = webServer.instance.buildUrl(
|
const registerUrl = webServer.instance.buildUrl(
|
||||||
`/enable_2fa_otp?token=&otpType=${otpType}&token=${token}`
|
`/enable_2fa_otp?token=${token}&otpType=${otpType}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const replaceTokens = (s) => {
|
const replaceTokens = (s) => {
|
||||||
|
@ -133,36 +138,48 @@ module.exports = class User2FA_OTPWebRegister
|
||||||
getTokenInfo(token, (err, tokenInfo) => {
|
getTokenInfo(token, (err, tokenInfo) => {
|
||||||
if(err) {
|
if(err) {
|
||||||
// assume expired
|
// 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') {
|
if(tokenInfo.tokenType !== 'auth_factor2_otp_register') {
|
||||||
return User2FA_OTPWebRegister.accessDenied(webServer, resp);
|
return User2FA_OTPWebRegister.accessDenied(webServer, resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
const qrImg = ''; // :TODO: fix me
|
const prepareOptions = {
|
||||||
const secret = '';
|
qrType : 'data',
|
||||||
const backupCodes = '';
|
cellSize : 8,
|
||||||
|
username : tokenInfo.user.username,
|
||||||
|
};
|
||||||
|
|
||||||
const postUrl = webServer.instance.buildUrl('/enable_2fa_otp');
|
prepareOTP(otpType, prepareOptions, (err, otpInfo) => {
|
||||||
const config = Config();
|
if(err) {
|
||||||
return webServer.instance.routeTemplateFilePage(
|
// :TODO: Log error
|
||||||
_.get(config, 'users.twoFactorAuth.otp.registerPageTemplate'),
|
return User2FA_OTPWebRegister.accessDenied(webServer, resp);
|
||||||
(templateData, next) => {
|
}
|
||||||
const finalPage = templateData
|
|
||||||
.replace(/%BOARDNAME%/g, config.general.boardName)
|
const postUrl = webServer.instance.buildUrl('/enable_2fa_otp');
|
||||||
.replace(/%USERNAME%/g, tokenInfo.user.username)
|
const config = Config();
|
||||||
.replace(/%TOKEN%/g, token)
|
return webServer.instance.routeTemplateFilePage(
|
||||||
.replace(/%OTP_TYPE%/g, otpType)
|
_.get(config, 'users.twoFactorAuth.otp.registerPageTemplate'),
|
||||||
.replace(/%POST_URL%/g, postUrl)
|
(templateData, next) => {
|
||||||
.replace(/%QR_IMG%/g, qrImg)
|
const finalPage = templateData
|
||||||
.replace(/%SECRET%/g, secret)
|
.replace(/%BOARDNAME%/g, config.general.boardName)
|
||||||
.replace(/%BACKUP_CODES%/g, backupCodes)
|
.replace(/%USERNAME%/g, tokenInfo.user.username)
|
||||||
;
|
.replace(/%TOKEN%/g, token)
|
||||||
return next(null, finalPage);
|
.replace(/%OTP_TYPE%/g, otpType)
|
||||||
},
|
.replace(/%POST_URL%/g, postUrl)
|
||||||
resp
|
.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', () => {
|
req.on('end', () => {
|
||||||
const formData = querystring.parse(bodyData);
|
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 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) {
|
static registerRoutes(cb) {
|
||||||
|
|
|
@ -5,33 +5,23 @@
|
||||||
<title>Enable 2FA/OTP — ENiGMA½ BBS</title>
|
<title>Enable 2FA/OTP — ENiGMA½ BBS</title>
|
||||||
<meta name="description" content="Enable 2-Factor Authentication via One-Time-Password">
|
<meta name="description" content="Enable 2-Factor Authentication via One-Time-Password">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<form action="%POST_URL%" method="post">
|
<p>
|
||||||
<legend>Enable One-Time-Password</legend>
|
Your OTP secret:<br>
|
||||||
<input type="text" placeholder="One Time Password" id="otp" name="otp" required>
|
<b>%SECRET%</b>
|
||||||
<input type="hidden" value="%TOKEN%" name="token">
|
</p>
|
||||||
<input type="hidden" value="%OTP_TYPE%" name="otpType">
|
<p>
|
||||||
<button type="submit">Confirm</button>
|
QR Code:<br>
|
||||||
</form>
|
<img src="%QR_IMG_DATA%"/>
|
||||||
|
</p>
|
||||||
|
<form action="%POST_URL%" method="post">
|
||||||
|
<legend>Confirm One-Time-Password to continue:</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">
|
||||||
|
<input type="hidden" value="%SECRET%" name="secret">
|
||||||
|
<button type="submit">Confirm</button>
|
||||||
|
</form>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
Loading…
Reference in New Issue