Initial real 2FA/OTP work
This commit is contained in:
@@ -846,6 +846,7 @@ function peg$parse(input, options) {
|
||||
|
||||
const UserProps = require('./user_property.js');
|
||||
const Log = require('./logger.js').log;
|
||||
const User = require('./user.js');
|
||||
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
@@ -982,6 +983,22 @@ function peg$parse(input, options) {
|
||||
SC : function isSecureConnection() {
|
||||
return _.get(client, 'session.isSecure', false);
|
||||
},
|
||||
AF : function currentAuthFactor() {
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
return !isNaN(value) && user.authFactor >= value;
|
||||
},
|
||||
AR : function authFactorRequired() {
|
||||
if(!user) {
|
||||
return false;
|
||||
}
|
||||
switch(value) {
|
||||
case 1 : return user.authFactor >= User.AuthFactors.Factor1;
|
||||
case 2 : return user.authFactor >= User.AuthFActors.Factor2;
|
||||
default : return false;
|
||||
}
|
||||
},
|
||||
ML : function minutesLeft() {
|
||||
// :TODO: implement me!
|
||||
return false;
|
||||
|
||||
@@ -224,6 +224,10 @@ function getDefaultConfig() {
|
||||
autoUnlockMinutes : 60 * 6, // 0=disabled; Auto unlock after N minutes.
|
||||
},
|
||||
unlockAtEmailPwReset : true, // if true, password reset via email will unlock locked accounts
|
||||
|
||||
twoFactorAuth : {
|
||||
method : 'googleAuth',
|
||||
}
|
||||
},
|
||||
|
||||
theme : {
|
||||
|
||||
@@ -107,7 +107,7 @@ function SSHClient(clientConn) {
|
||||
};
|
||||
|
||||
const authWithPasswordOrPubKey = (authType) => {
|
||||
if(User.AuthFactor1Types.PubKey !== authType || !self.user.isAuthenticated() || !ctx.signature) {
|
||||
if(User.AuthFactor1Types.SSHPubKey !== authType || !self.user.isAuthenticated() || !ctx.signature) {
|
||||
// step 1: login/auth using PubKey
|
||||
userLogin(self, ctx.username, ctx.password, { authType, ctx }, (err) => {
|
||||
if(err) {
|
||||
@@ -126,7 +126,7 @@ function SSHClient(clientConn) {
|
||||
});
|
||||
} else {
|
||||
// step 2: verify signature
|
||||
const pubKeyActual = ssh2.utils.parseKey(self.user.getProperty(UserProps.LoginPubKey));
|
||||
const pubKeyActual = ssh2.utils.parseKey(self.user.getProperty(UserProps.AuthPubKey));
|
||||
if(!pubKeyActual || !pubKeyActual.verify(ctx.blob, ctx.signature)) {
|
||||
return slowTerminateConnection();
|
||||
}
|
||||
@@ -191,7 +191,7 @@ function SSHClient(clientConn) {
|
||||
//return authWithPassword();
|
||||
|
||||
case 'publickey' :
|
||||
return authWithPasswordOrPubKey(User.AuthFactor1Types.PubKey);
|
||||
return authWithPasswordOrPubKey(User.AuthFactor1Types.SSHPubKey);
|
||||
//return authWithPubKey();
|
||||
|
||||
case 'keyboard-interactive' :
|
||||
|
||||
@@ -8,12 +8,14 @@ const { userLogin } = require('./user_login.js');
|
||||
const messageArea = require('./message_area.js');
|
||||
const { ErrorReasons } = require('./enig_error.js');
|
||||
const UserProps = require('./user_property.js');
|
||||
const { user2FA_OTP } = require('./user_2fa_otp.js');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
const iconv = require('iconv-lite');
|
||||
|
||||
exports.login = login;
|
||||
exports.login2FA_OTP = login2FA_OTP;
|
||||
exports.logoff = logoff;
|
||||
exports.prevMenu = prevMenu;
|
||||
exports.nextMenu = nextMenu;
|
||||
@@ -23,32 +25,47 @@ exports.prevArea = prevArea;
|
||||
exports.nextArea = nextArea;
|
||||
exports.sendForgotPasswordEmail = sendForgotPasswordEmail;
|
||||
|
||||
const handleAuthFailures = (callingMenu, err, cb) => {
|
||||
// already logged in with this user?
|
||||
if(ErrorReasons.AlreadyLoggedIn === err.reasonCode &&
|
||||
_.has(callingMenu, 'menuConfig.config.tooNodeMenu'))
|
||||
{
|
||||
return callingMenu.gotoMenu(callingMenu.menuConfig.config.tooNodeMenu, cb);
|
||||
}
|
||||
|
||||
// banned username results in disconnect
|
||||
if(ErrorReasons.NotAllowed === err.reasonCode) {
|
||||
return logoff(callingMenu, {}, {}, cb);
|
||||
}
|
||||
|
||||
const ReasonsMenus = [
|
||||
ErrorReasons.TooMany, ErrorReasons.Disabled, ErrorReasons.Inactive, ErrorReasons.Locked
|
||||
];
|
||||
if(ReasonsMenus.includes(err.reasonCode)) {
|
||||
const menu = _.get(callingMenu, [ 'menuConfig', 'config', err.reasonCode.toLowerCase() ]);
|
||||
return menu ? callingMenu.gotoMenu(menu, cb) : logoff(callingMenu, {}, {}, cb);
|
||||
}
|
||||
|
||||
// Other error
|
||||
return callingMenu.prevMenu(cb);
|
||||
};
|
||||
|
||||
function login(callingMenu, formData, extraArgs, cb) {
|
||||
|
||||
userLogin(callingMenu.client, formData.value.username, formData.value.password, err => {
|
||||
if(err) {
|
||||
// already logged in with this user?
|
||||
if(ErrorReasons.AlreadyLoggedIn === err.reasonCode &&
|
||||
_.has(callingMenu, 'menuConfig.config.tooNodeMenu'))
|
||||
{
|
||||
return callingMenu.gotoMenu(callingMenu.menuConfig.config.tooNodeMenu, cb);
|
||||
}
|
||||
return handleAuthFailures(callingMenu, err, cb);
|
||||
}
|
||||
|
||||
// banned username results in disconnect
|
||||
if(ErrorReasons.NotAllowed === err.reasonCode) {
|
||||
return logoff(callingMenu, {}, {}, cb);
|
||||
}
|
||||
// success!
|
||||
return callingMenu.nextMenu(cb);
|
||||
});
|
||||
}
|
||||
|
||||
const ReasonsMenus = [
|
||||
ErrorReasons.TooMany, ErrorReasons.Disabled, ErrorReasons.Inactive, ErrorReasons.Locked
|
||||
];
|
||||
if(ReasonsMenus.includes(err.reasonCode)) {
|
||||
const menu = _.get(callingMenu, [ 'menuConfig', 'config', err.reasonCode.toLowerCase() ]);
|
||||
return menu ? callingMenu.gotoMenu(menu, cb) : logoff(callingMenu, {}, {}, cb);
|
||||
}
|
||||
|
||||
// Other error
|
||||
return callingMenu.prevMenu(cb);
|
||||
function login2FA_OTP(callingMenu, formData, extraArgs, cb) {
|
||||
user2FA_OTP(callingMenu.client, formData.value.token, err => {
|
||||
if(err) {
|
||||
return handleAuthFailures(callingMenu, err, cb);
|
||||
}
|
||||
|
||||
// success!
|
||||
|
||||
38
core/user.js
38
core/user.js
@@ -27,10 +27,11 @@ exports.isRootUserId = function(id) { return 1 === id; };
|
||||
|
||||
module.exports = class User {
|
||||
constructor() {
|
||||
this.userId = 0;
|
||||
this.username = '';
|
||||
this.properties = {}; // name:value
|
||||
this.groups = []; // group membership(s)
|
||||
this.userId = 0;
|
||||
this.username = '';
|
||||
this.properties = {}; // name:value
|
||||
this.groups = []; // group membership(s)
|
||||
this.authFactor = User.AuthFactors.None;
|
||||
}
|
||||
|
||||
// static property accessors
|
||||
@@ -38,6 +39,14 @@ module.exports = class User {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static get AuthFactors() {
|
||||
return {
|
||||
None : 0, // Not yet authenticated in any way
|
||||
Factor1 : 1, // username + password/pubkey/etc. checked out
|
||||
Factor2 : 2, // validated with 2FA of some sort such as OTP
|
||||
};
|
||||
}
|
||||
|
||||
static get PBKDF2() {
|
||||
return {
|
||||
iterations : 1000,
|
||||
@@ -50,7 +59,7 @@ module.exports = class User {
|
||||
return {
|
||||
auth : [
|
||||
UserProps.PassPbkdf2Salt, UserProps.PassPbkdf2Dk,
|
||||
UserProps.LoginPubKey, UserProps.LoginPubKeyFingerprintSHA256,
|
||||
UserProps.AuthPubKey,
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -180,8 +189,9 @@ module.exports = class User {
|
||||
|
||||
static get AuthFactor1Types() {
|
||||
return {
|
||||
PubKey : 'pubKey',
|
||||
SSHPubKey : 'sshPubKey',
|
||||
Password : 'password',
|
||||
TLSClient : 'tlsClientAuth',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -210,7 +220,7 @@ module.exports = class User {
|
||||
};
|
||||
|
||||
const validatePubKey = (props, callback) => {
|
||||
const pubKeyActual = ssh2.utils.parseKey(props[UserProps.LoginPubKey]);
|
||||
const pubKeyActual = ssh2.utils.parseKey(props[UserProps.AuthPubKey]);
|
||||
if(!pubKeyActual) {
|
||||
return callback(Errors.AccessDenied('Invalid public key'));
|
||||
}
|
||||
@@ -242,7 +252,7 @@ module.exports = class User {
|
||||
});
|
||||
},
|
||||
function validatePassOrPubKey(props, callback) {
|
||||
if(User.AuthFactor1Types.PubKey === authInfo.type) {
|
||||
if(User.AuthFactor1Types.SSHPubKey === authInfo.type) {
|
||||
return validatePubKey(props, callback);
|
||||
}
|
||||
return validatePassword(props, callback);
|
||||
@@ -323,7 +333,12 @@ module.exports = class User {
|
||||
self.username = tempAuthInfo.username;
|
||||
self.properties = tempAuthInfo.properties;
|
||||
self.groups = tempAuthInfo.groups;
|
||||
self.authenticated = true;
|
||||
self.authFactor = User.AuthFactors.Factor1;
|
||||
|
||||
//
|
||||
// If 2FA/OTP is required, this user is not quite authenticated yet.
|
||||
//
|
||||
self.authenticated = !(self.getProperty(UserProps.AuthFactor2OTP) ? true : false);
|
||||
|
||||
self.removeProperty(UserProps.FailedLoginAttempts);
|
||||
|
||||
@@ -604,7 +619,10 @@ module.exports = class User {
|
||||
user.username = userName;
|
||||
user.properties = properties;
|
||||
user.groups = groups;
|
||||
user.authenticated = false; // this is NOT an authenticated user!
|
||||
|
||||
// explicitly NOT an authenticated user!
|
||||
user.authenticated = false;
|
||||
user.authFactor = User.AuthFactors.None;
|
||||
|
||||
return cb(err, user);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,10 @@ module.exports = {
|
||||
|
||||
MinutesOnlineTotalCount : 'minutes_online_total_count',
|
||||
|
||||
LoginPubKey : 'login_public_key', // OpenSSL format
|
||||
//LoginPubKeyFingerprintSHA256 : 'login_public_key_fp_sha256', // hint: ssh-kegen -lf id_rsa.pub
|
||||
SSHPubKey : 'ssh_public_key', // OpenSSH format (ssh-keygen, etc.)
|
||||
AuthFactor1Types : 'auth_factor1_types', // List of User.AuthFactor1Types value(s)
|
||||
AuthFactor2OTP : 'auth_factor2_otp', // If present, OTP type for 2FA
|
||||
AuthFactor2OTPSecret : 'auth_factor2_otp_secret', // Secret used in conjunction with OTP 2FA
|
||||
AuthFactor2OTPScratchCodes : 'auth_factor2_otp_scratch', // JSON array style codes ["code1", "code2", ...]
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user