Good progress on QR support
This commit is contained in:
parent
3c6c8d2a5c
commit
6070bc94e7
|
@ -16,6 +16,7 @@ const UserProps = require('../user_property.js');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
|
||||||
exports.handleUserCommand = handleUserCommand;
|
exports.handleUserCommand = handleUserCommand;
|
||||||
|
|
||||||
|
@ -343,6 +344,62 @@ Affiliations : ${propOrNA(UserProps.Affiliations)}
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function twoFactorAuth(user) {
|
||||||
|
if(argv._.length < 4) {
|
||||||
|
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
OTPTypes,
|
||||||
|
prepareOTP,
|
||||||
|
} = require('../../core/user_2fa_otp.js');
|
||||||
|
|
||||||
|
async.waterfall(
|
||||||
|
[
|
||||||
|
function validate(callback) {
|
||||||
|
// :TODO: Prompt for if not supplied
|
||||||
|
let otpType = argv._[argv._.length - 1];
|
||||||
|
otpType = _.find(OTPTypes, t => {
|
||||||
|
return t.toLowerCase() === otpType;
|
||||||
|
});
|
||||||
|
if(!otpType) {
|
||||||
|
return callback(Errors.Invalid('Invalid OTP type'));
|
||||||
|
}
|
||||||
|
return callback(null, otpType);
|
||||||
|
},
|
||||||
|
function prepare(otpType, callback) {
|
||||||
|
const otpOpts = {
|
||||||
|
username : user.username,
|
||||||
|
qrType : argv['qr-type'] || 'ascii',
|
||||||
|
};
|
||||||
|
prepareOTP(otpType, otpOpts, (err, otpInfo) => {
|
||||||
|
return callback(err, otpInfo);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function storeOrDisplayQR(otpInfo, callback) {
|
||||||
|
if(!argv.out) {
|
||||||
|
return callback(null, otpInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if('-' === argv.out) {
|
||||||
|
console.info(otpInfo.qr);
|
||||||
|
return callback(null, otpInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFile(argv.out, otpInfo.qr, 'utf8', err => {
|
||||||
|
return callback(err, otpInfo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
],
|
||||||
|
(err) => {
|
||||||
|
if(err) {
|
||||||
|
console.error(err.message);
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function handleUserCommand() {
|
function handleUserCommand() {
|
||||||
function errUsage() {
|
function errUsage() {
|
||||||
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
|
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
|
||||||
|
@ -356,7 +413,8 @@ function handleUserCommand() {
|
||||||
const usernameIdx = [
|
const usernameIdx = [
|
||||||
'pw', 'pass', 'passwd', 'password',
|
'pw', 'pass', 'passwd', 'password',
|
||||||
'group',
|
'group',
|
||||||
'mv', 'rename'
|
'mv', 'rename',
|
||||||
|
'2fa',
|
||||||
].includes(action) ? argv._.length - 2 : argv._.length - 1;
|
].includes(action) ? argv._.length - 2 : argv._.length - 1;
|
||||||
const userName = argv._[usernameIdx];
|
const userName = argv._[usernameIdx];
|
||||||
|
|
||||||
|
@ -391,6 +449,8 @@ function handleUserCommand() {
|
||||||
group : modUserGroups,
|
group : modUserGroups,
|
||||||
|
|
||||||
info : showUserInfo,
|
info : showUserInfo,
|
||||||
|
|
||||||
|
'2fa' : twoFactorAuth,
|
||||||
}[action] || errUsage)(user, action);
|
}[action] || errUsage)(user, action);
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -12,14 +12,16 @@ const {
|
||||||
recordLogin,
|
recordLogin,
|
||||||
transformLoginError,
|
transformLoginError,
|
||||||
} = require('./user_login.js');
|
} = require('./user_login.js');
|
||||||
|
const Config = require('./config.js').get;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
const qrGen = require('qrcode-generator');
|
||||||
|
|
||||||
exports.loginFactor2_OTP = loginFactor2_OTP;
|
exports.prepareOTP = prepareOTP;
|
||||||
exports.generateNewBackupCodes = generateNewBackupCodes;
|
exports.loginFactor2_OTP = loginFactor2_OTP;
|
||||||
|
|
||||||
const OTPTypes = exports.OTPTypes = {
|
const OTPTypes = exports.OTPTypes = {
|
||||||
RFC6238_TOTP : 'rfc6238_TOTP', // Time-Based, SHA-512
|
RFC6238_TOTP : 'rfc6238_TOTP', // Time-Based, SHA-512
|
||||||
|
@ -28,23 +30,27 @@ const OTPTypes = exports.OTPTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function otpFromType(otpType) {
|
function otpFromType(otpType) {
|
||||||
return {
|
try {
|
||||||
[ OTPTypes.RFC6238_TOTP ] : () => {
|
return {
|
||||||
const totp = require('otplib/totp');
|
[ OTPTypes.RFC6238_TOTP ] : () => {
|
||||||
totp.options = { crypto, algorithm : 'sha256' };
|
const totp = require('otplib/totp');
|
||||||
return totp;
|
totp.options = { crypto, algorithm : 'sha256' };
|
||||||
},
|
return totp;
|
||||||
[ OTPTypes.RFC4266_HOTP ] : () => {
|
},
|
||||||
const hotp = require('otplib/hotp');
|
[ OTPTypes.RFC4266_HOTP ] : () => {
|
||||||
hotp.options = { crypto, algorithm : 'sha256' };
|
const hotp = require('otplib/hotp');
|
||||||
return hotp;
|
hotp.options = { crypto, algorithm : 'sha256' };
|
||||||
},
|
return hotp;
|
||||||
[ OTPTypes.GoogleAuthenticator ] : () => {
|
},
|
||||||
const googleAuth = require('otplib/authenticator');
|
[ OTPTypes.GoogleAuthenticator ] : () => {
|
||||||
googleAuth.options = { crypto };
|
const googleAuth = require('otplib/authenticator');
|
||||||
return googleAuth;
|
googleAuth.options = { crypto };
|
||||||
},
|
return googleAuth;
|
||||||
}[otpType]();
|
},
|
||||||
|
}[otpType]();
|
||||||
|
} catch(e) {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateOTPBackupCode() {
|
function generateOTPBackupCode() {
|
||||||
|
@ -79,13 +85,9 @@ function backupCodePBKDF2(secret, salt, cb) {
|
||||||
return crypto.pbkdf2(secret, salt, 1000, 128, 'sha1', cb);
|
return crypto.pbkdf2(secret, salt, 1000, 128, 'sha1', cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateNewBackupCodes(user, cb) {
|
function generateNewBackupCodes(cb) {
|
||||||
//
|
const plainTextCodes = [...Array(6)].map(() => generateOTPBackupCode());
|
||||||
// Backup codes are not stored in plain text, but rather
|
async.map(plainTextCodes, (code, nextCode) => {
|
||||||
// an array of objects: [{salt, code}, ...]
|
|
||||||
//
|
|
||||||
const plainCodes = [...Array(6)].map(() => generateOTPBackupCode());
|
|
||||||
async.map(plainCodes, (code, nextCode) => {
|
|
||||||
crypto.randomBytes(16, (err, salt) => {
|
crypto.randomBytes(16, (err, salt) => {
|
||||||
if(err) {
|
if(err) {
|
||||||
return nextCode(err);
|
return nextCode(err);
|
||||||
|
@ -101,14 +103,7 @@ function generateNewBackupCodes(user, cb) {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(err, codes) => {
|
(err, codes) => {
|
||||||
if(err) {
|
return cb(err, codes, plainTextCodes);
|
||||||
return cb(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
codes = JSON.stringify(codes);
|
|
||||||
user.persistProperty(UserProps.AuthFactor2OTPBackupCodes, codes, err => {
|
|
||||||
return cb(err, plainCodes);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,13 +144,51 @@ function validateAndConsumeBackupCode(user, token, cb) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createQRCode(otp, options, secret) {
|
||||||
|
const uri = otp.keyuri(options.username || 'user', Config().general.boardName, secret);
|
||||||
|
const qrCode = qrGen(0, 'L');
|
||||||
|
qrCode.addData(uri);
|
||||||
|
qrCode.make();
|
||||||
|
|
||||||
|
options.qrType = options.qrType || 'ascii';
|
||||||
|
return {
|
||||||
|
ascii : qrCode.createASCII,
|
||||||
|
data : qrCode.createDataURL,
|
||||||
|
img : qrCode.createImgTag,
|
||||||
|
svg : qrCode.createSvgTag,
|
||||||
|
}[options.qrType]();
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareOTP(otpType, options, cb) {
|
||||||
|
if(!_.isFunction(cb)) {
|
||||||
|
cb = options;
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const otp = otpFromType(otpType);
|
||||||
|
if(!otp) {
|
||||||
|
return cb(Errors.Invalid(`Unknown OTP type: ${otpType}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = OTPTypes.GoogleAuthenticator === otpType ?
|
||||||
|
otp.generateSecret() :
|
||||||
|
crypto.randomBytes(64).toString('base64').substr(0, 32);
|
||||||
|
|
||||||
|
generateNewBackupCodes((err, codes, plainTextCodes) => {
|
||||||
|
const qr = createQRCode(otp, options, secret);
|
||||||
|
return cb(err, { secret, codes, plainTextCodes, qr } );
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function loginFactor2_OTP(client, token, cb) {
|
function loginFactor2_OTP(client, token, cb) {
|
||||||
if(client.user.authFactor < User.AuthFactors.Factor1) {
|
if(client.user.authFactor < User.AuthFactors.Factor1) {
|
||||||
return cb(Errors.AccessDenied('OTP requires prior authentication factor 1'));
|
return cb(Errors.AccessDenied('OTP requires prior authentication factor 1'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const otpType = client.user.getProperty(UserProps.AuthFactor2OTP);
|
const otpType = client.user.getProperty(UserProps.AuthFactor2OTP);
|
||||||
if(!_.values(OTPTypes).includes(otpType)) {
|
const otp = otpFromType(otpType);
|
||||||
|
|
||||||
|
if(!otp) {
|
||||||
return cb(Errors.Invalid(`Unknown OTP type: ${otpType}`));
|
return cb(Errors.Invalid(`Unknown OTP type: ${otpType}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,7 +197,6 @@ function loginFactor2_OTP(client, token, cb) {
|
||||||
return cb(Errors.Invalid('Missing OTP secret'));
|
return cb(Errors.Invalid('Missing OTP secret'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const otp = otpFromType(otpType);
|
|
||||||
const valid = otp.verify( { token, secret } );
|
const valid = otp.verify( { token, secret } );
|
||||||
|
|
||||||
const allowLogin = () => {
|
const allowLogin = () => {
|
||||||
|
|
|
@ -20,6 +20,7 @@ const User = require('./user.js');
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
exports.userLogin = userLogin;
|
exports.userLogin = userLogin;
|
||||||
exports.recordLogin = recordLogin;
|
exports.recordLogin = recordLogin;
|
||||||
|
@ -110,6 +111,8 @@ function userLogin(client, username, password, options, cb) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function recordLogin(client, cb) {
|
function recordLogin(client, cb) {
|
||||||
|
assert(client.user.authenticated); // don't get in situations where this isn't true
|
||||||
|
|
||||||
const user = client.user;
|
const user = client.user;
|
||||||
async.parallel(
|
async.parallel(
|
||||||
[
|
[
|
||||||
|
|
|
@ -55,7 +55,8 @@
|
||||||
"ws": "^6.1.3",
|
"ws": "^6.1.3",
|
||||||
"xxhash": "^0.2.4",
|
"xxhash": "^0.2.4",
|
||||||
"yazl": "^2.5.1",
|
"yazl": "^2.5.1",
|
||||||
"otplib": "^10.0.1"
|
"otplib": "^10.0.1",
|
||||||
|
"qrcode-generator": "1.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {},
|
"devDependencies": {},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
@ -1570,6 +1570,11 @@ punycode@^1.4.1:
|
||||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
||||||
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
|
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
|
||||||
|
|
||||||
|
qrcode-generator@1.4.3:
|
||||||
|
version "1.4.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/qrcode-generator/-/qrcode-generator-1.4.3.tgz#4876e8f280e65b6c94615f4c19c484f6b964b199"
|
||||||
|
integrity sha512-++rVRvMRq5BlHfmAafl8a4ppUntzUxCCUTT2t0siUgqKwdnqRzY8IH6f6WSX5dZUhD2Ul5/MIKuTJddflwrGzw==
|
||||||
|
|
||||||
qs@~6.5.2:
|
qs@~6.5.2:
|
||||||
version "6.5.2"
|
version "6.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||||
|
|
Loading…
Reference in New Issue