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 _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
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 errUsage() {
|
||||
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
|
||||
|
@ -356,7 +413,8 @@ function handleUserCommand() {
|
|||
const usernameIdx = [
|
||||
'pw', 'pass', 'passwd', 'password',
|
||||
'group',
|
||||
'mv', 'rename'
|
||||
'mv', 'rename',
|
||||
'2fa',
|
||||
].includes(action) ? argv._.length - 2 : argv._.length - 1;
|
||||
const userName = argv._[usernameIdx];
|
||||
|
||||
|
@ -391,6 +449,8 @@ function handleUserCommand() {
|
|||
group : modUserGroups,
|
||||
|
||||
info : showUserInfo,
|
||||
|
||||
'2fa' : twoFactorAuth,
|
||||
}[action] || errUsage)(user, action);
|
||||
});
|
||||
}
|
|
@ -12,14 +12,16 @@ const {
|
|||
recordLogin,
|
||||
transformLoginError,
|
||||
} = require('./user_login.js');
|
||||
const Config = require('./config.js').get;
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
const crypto = require('crypto');
|
||||
const async = require('async');
|
||||
const qrGen = require('qrcode-generator');
|
||||
|
||||
exports.loginFactor2_OTP = loginFactor2_OTP;
|
||||
exports.generateNewBackupCodes = generateNewBackupCodes;
|
||||
exports.prepareOTP = prepareOTP;
|
||||
exports.loginFactor2_OTP = loginFactor2_OTP;
|
||||
|
||||
const OTPTypes = exports.OTPTypes = {
|
||||
RFC6238_TOTP : 'rfc6238_TOTP', // Time-Based, SHA-512
|
||||
|
@ -28,23 +30,27 @@ const OTPTypes = exports.OTPTypes = {
|
|||
};
|
||||
|
||||
function otpFromType(otpType) {
|
||||
return {
|
||||
[ OTPTypes.RFC6238_TOTP ] : () => {
|
||||
const totp = require('otplib/totp');
|
||||
totp.options = { crypto, algorithm : 'sha256' };
|
||||
return totp;
|
||||
},
|
||||
[ OTPTypes.RFC4266_HOTP ] : () => {
|
||||
const hotp = require('otplib/hotp');
|
||||
hotp.options = { crypto, algorithm : 'sha256' };
|
||||
return hotp;
|
||||
},
|
||||
[ OTPTypes.GoogleAuthenticator ] : () => {
|
||||
const googleAuth = require('otplib/authenticator');
|
||||
googleAuth.options = { crypto };
|
||||
return googleAuth;
|
||||
},
|
||||
}[otpType]();
|
||||
try {
|
||||
return {
|
||||
[ OTPTypes.RFC6238_TOTP ] : () => {
|
||||
const totp = require('otplib/totp');
|
||||
totp.options = { crypto, algorithm : 'sha256' };
|
||||
return totp;
|
||||
},
|
||||
[ OTPTypes.RFC4266_HOTP ] : () => {
|
||||
const hotp = require('otplib/hotp');
|
||||
hotp.options = { crypto, algorithm : 'sha256' };
|
||||
return hotp;
|
||||
},
|
||||
[ OTPTypes.GoogleAuthenticator ] : () => {
|
||||
const googleAuth = require('otplib/authenticator');
|
||||
googleAuth.options = { crypto };
|
||||
return googleAuth;
|
||||
},
|
||||
}[otpType]();
|
||||
} catch(e) {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
|
||||
function generateOTPBackupCode() {
|
||||
|
@ -79,13 +85,9 @@ function backupCodePBKDF2(secret, salt, cb) {
|
|||
return crypto.pbkdf2(secret, salt, 1000, 128, 'sha1', cb);
|
||||
}
|
||||
|
||||
function generateNewBackupCodes(user, cb) {
|
||||
//
|
||||
// Backup codes are not stored in plain text, but rather
|
||||
// an array of objects: [{salt, code}, ...]
|
||||
//
|
||||
const plainCodes = [...Array(6)].map(() => generateOTPBackupCode());
|
||||
async.map(plainCodes, (code, nextCode) => {
|
||||
function generateNewBackupCodes(cb) {
|
||||
const plainTextCodes = [...Array(6)].map(() => generateOTPBackupCode());
|
||||
async.map(plainTextCodes, (code, nextCode) => {
|
||||
crypto.randomBytes(16, (err, salt) => {
|
||||
if(err) {
|
||||
return nextCode(err);
|
||||
|
@ -101,14 +103,7 @@ function generateNewBackupCodes(user, cb) {
|
|||
});
|
||||
},
|
||||
(err, codes) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
codes = JSON.stringify(codes);
|
||||
user.persistProperty(UserProps.AuthFactor2OTPBackupCodes, codes, err => {
|
||||
return cb(err, plainCodes);
|
||||
});
|
||||
return cb(err, codes, plainTextCodes);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
if(client.user.authFactor < User.AuthFactors.Factor1) {
|
||||
return cb(Errors.AccessDenied('OTP requires prior authentication factor 1'));
|
||||
}
|
||||
|
||||
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}`));
|
||||
}
|
||||
|
||||
|
@ -164,7 +197,6 @@ function loginFactor2_OTP(client, token, cb) {
|
|||
return cb(Errors.Invalid('Missing OTP secret'));
|
||||
}
|
||||
|
||||
const otp = otpFromType(otpType);
|
||||
const valid = otp.verify( { token, secret } );
|
||||
|
||||
const allowLogin = () => {
|
||||
|
|
|
@ -20,6 +20,7 @@ const User = require('./user.js');
|
|||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const assert = require('assert');
|
||||
|
||||
exports.userLogin = userLogin;
|
||||
exports.recordLogin = recordLogin;
|
||||
|
@ -110,6 +111,8 @@ function userLogin(client, username, password, options, cb) {
|
|||
}
|
||||
|
||||
function recordLogin(client, cb) {
|
||||
assert(client.user.authenticated); // don't get in situations where this isn't true
|
||||
|
||||
const user = client.user;
|
||||
async.parallel(
|
||||
[
|
||||
|
|
|
@ -55,7 +55,8 @@
|
|||
"ws": "^6.1.3",
|
||||
"xxhash": "^0.2.4",
|
||||
"yazl": "^2.5.1",
|
||||
"otplib": "^10.0.1"
|
||||
"otplib": "^10.0.1",
|
||||
"qrcode-generator": "1.4.3"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"engines": {
|
||||
|
|
|
@ -1570,6 +1570,11 @@ punycode@^1.4.1:
|
|||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
||||
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:
|
||||
version "6.5.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
|
|
Loading…
Reference in New Issue