Initial real 2FA/OTP work

This commit is contained in:
Bryan Ashby 2019-04-09 20:07:19 -06:00
parent 75d6eef92f
commit 0ed507cd7b
No known key found for this signature in database
GPG Key ID: B49EB437951D2542
13 changed files with 133 additions and 39 deletions

View File

@ -3,7 +3,9 @@
"es6": true, "es6": true,
"node": true "node": true
}, },
"extends": "eslint:recommended", "extends": [
"eslint:recommended"
],
"rules": { "rules": {
"indent": [ "indent": [
"error", "error",

View File

@ -4,6 +4,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For
## 0.0.10-alpha ## 0.0.10-alpha
+ `oputil.js user rename USERNAME NEWNAME` + `oputil.js user rename USERNAME NEWNAME`
+ `my_messages.js` module (defaulted to "m" at the message menu) to list public messages addressed to the currently logged in user. Takes into account their username and `real_name` property. + `my_messages.js` module (defaulted to "m" at the message menu) to list public messages addressed to the currently logged in user. Takes into account their username and `real_name` property.
+ SSH Public Key Authentication has been added. The system uses a OpenSSH style public key set on the `ssh_public_key` user property.
+
## 0.0.9-alpha ## 0.0.9-alpha

View File

@ -846,6 +846,7 @@ function peg$parse(input, options) {
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const User = require('./user.js');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
@ -982,6 +983,22 @@ function peg$parse(input, options) {
SC : function isSecureConnection() { SC : function isSecureConnection() {
return _.get(client, 'session.isSecure', false); 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() { ML : function minutesLeft() {
// :TODO: implement me! // :TODO: implement me!
return false; return false;

View File

@ -224,6 +224,10 @@ function getDefaultConfig() {
autoUnlockMinutes : 60 * 6, // 0=disabled; Auto unlock after N minutes. autoUnlockMinutes : 60 * 6, // 0=disabled; Auto unlock after N minutes.
}, },
unlockAtEmailPwReset : true, // if true, password reset via email will unlock locked accounts unlockAtEmailPwReset : true, // if true, password reset via email will unlock locked accounts
twoFactorAuth : {
method : 'googleAuth',
}
}, },
theme : { theme : {

View File

@ -107,7 +107,7 @@ function SSHClient(clientConn) {
}; };
const authWithPasswordOrPubKey = (authType) => { 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 // step 1: login/auth using PubKey
userLogin(self, ctx.username, ctx.password, { authType, ctx }, (err) => { userLogin(self, ctx.username, ctx.password, { authType, ctx }, (err) => {
if(err) { if(err) {
@ -126,7 +126,7 @@ function SSHClient(clientConn) {
}); });
} else { } else {
// step 2: verify signature // 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)) { if(!pubKeyActual || !pubKeyActual.verify(ctx.blob, ctx.signature)) {
return slowTerminateConnection(); return slowTerminateConnection();
} }
@ -191,7 +191,7 @@ function SSHClient(clientConn) {
//return authWithPassword(); //return authWithPassword();
case 'publickey' : case 'publickey' :
return authWithPasswordOrPubKey(User.AuthFactor1Types.PubKey); return authWithPasswordOrPubKey(User.AuthFactor1Types.SSHPubKey);
//return authWithPubKey(); //return authWithPubKey();
case 'keyboard-interactive' : case 'keyboard-interactive' :

View File

@ -8,12 +8,14 @@ const { userLogin } = require('./user_login.js');
const messageArea = require('./message_area.js'); const messageArea = require('./message_area.js');
const { ErrorReasons } = require('./enig_error.js'); const { ErrorReasons } = require('./enig_error.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
const { user2FA_OTP } = require('./user_2fa_otp.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const iconv = require('iconv-lite'); const iconv = require('iconv-lite');
exports.login = login; exports.login = login;
exports.login2FA_OTP = login2FA_OTP;
exports.logoff = logoff; exports.logoff = logoff;
exports.prevMenu = prevMenu; exports.prevMenu = prevMenu;
exports.nextMenu = nextMenu; exports.nextMenu = nextMenu;
@ -23,32 +25,47 @@ exports.prevArea = prevArea;
exports.nextArea = nextArea; exports.nextArea = nextArea;
exports.sendForgotPasswordEmail = sendForgotPasswordEmail; 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) { function login(callingMenu, formData, extraArgs, cb) {
userLogin(callingMenu.client, formData.value.username, formData.value.password, err => { userLogin(callingMenu.client, formData.value.username, formData.value.password, err => {
if(err) { if(err) {
// already logged in with this user? return handleAuthFailures(callingMenu, err, cb);
if(ErrorReasons.AlreadyLoggedIn === err.reasonCode && }
_.has(callingMenu, 'menuConfig.config.tooNodeMenu'))
{
return callingMenu.gotoMenu(callingMenu.menuConfig.config.tooNodeMenu, cb);
}
// banned username results in disconnect // success!
if(ErrorReasons.NotAllowed === err.reasonCode) { return callingMenu.nextMenu(cb);
return logoff(callingMenu, {}, {}, cb); });
} }
const ReasonsMenus = [ function login2FA_OTP(callingMenu, formData, extraArgs, cb) {
ErrorReasons.TooMany, ErrorReasons.Disabled, ErrorReasons.Inactive, ErrorReasons.Locked user2FA_OTP(callingMenu.client, formData.value.token, err => {
]; if(err) {
if(ReasonsMenus.includes(err.reasonCode)) { return handleAuthFailures(callingMenu, err, cb);
const menu = _.get(callingMenu, [ 'menuConfig', 'config', err.reasonCode.toLowerCase() ]);
return menu ? callingMenu.gotoMenu(menu, cb) : logoff(callingMenu, {}, {}, cb);
}
// Other error
return callingMenu.prevMenu(cb);
} }
// success! // success!

View File

@ -27,10 +27,11 @@ exports.isRootUserId = function(id) { return 1 === id; };
module.exports = class User { module.exports = class User {
constructor() { constructor() {
this.userId = 0; this.userId = 0;
this.username = ''; this.username = '';
this.properties = {}; // name:value this.properties = {}; // name:value
this.groups = []; // group membership(s) this.groups = []; // group membership(s)
this.authFactor = User.AuthFactors.None;
} }
// static property accessors // static property accessors
@ -38,6 +39,14 @@ module.exports = class User {
return 1; 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() { static get PBKDF2() {
return { return {
iterations : 1000, iterations : 1000,
@ -50,7 +59,7 @@ module.exports = class User {
return { return {
auth : [ auth : [
UserProps.PassPbkdf2Salt, UserProps.PassPbkdf2Dk, UserProps.PassPbkdf2Salt, UserProps.PassPbkdf2Dk,
UserProps.LoginPubKey, UserProps.LoginPubKeyFingerprintSHA256, UserProps.AuthPubKey,
], ],
}; };
} }
@ -180,8 +189,9 @@ module.exports = class User {
static get AuthFactor1Types() { static get AuthFactor1Types() {
return { return {
PubKey : 'pubKey', SSHPubKey : 'sshPubKey',
Password : 'password', Password : 'password',
TLSClient : 'tlsClientAuth',
}; };
} }
@ -210,7 +220,7 @@ module.exports = class User {
}; };
const validatePubKey = (props, callback) => { const validatePubKey = (props, callback) => {
const pubKeyActual = ssh2.utils.parseKey(props[UserProps.LoginPubKey]); const pubKeyActual = ssh2.utils.parseKey(props[UserProps.AuthPubKey]);
if(!pubKeyActual) { if(!pubKeyActual) {
return callback(Errors.AccessDenied('Invalid public key')); return callback(Errors.AccessDenied('Invalid public key'));
} }
@ -242,7 +252,7 @@ module.exports = class User {
}); });
}, },
function validatePassOrPubKey(props, callback) { function validatePassOrPubKey(props, callback) {
if(User.AuthFactor1Types.PubKey === authInfo.type) { if(User.AuthFactor1Types.SSHPubKey === authInfo.type) {
return validatePubKey(props, callback); return validatePubKey(props, callback);
} }
return validatePassword(props, callback); return validatePassword(props, callback);
@ -323,7 +333,12 @@ module.exports = class User {
self.username = tempAuthInfo.username; self.username = tempAuthInfo.username;
self.properties = tempAuthInfo.properties; self.properties = tempAuthInfo.properties;
self.groups = tempAuthInfo.groups; 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); self.removeProperty(UserProps.FailedLoginAttempts);
@ -604,7 +619,10 @@ module.exports = class User {
user.username = userName; user.username = userName;
user.properties = properties; user.properties = properties;
user.groups = groups; 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); return cb(err, user);
} }

View File

@ -58,7 +58,10 @@ module.exports = {
MinutesOnlineTotalCount : 'minutes_online_total_count', MinutesOnlineTotalCount : 'minutes_online_total_count',
LoginPubKey : 'login_public_key', // OpenSSL format SSHPubKey : 'ssh_public_key', // OpenSSH format (ssh-keygen, etc.)
//LoginPubKeyFingerprintSHA256 : 'login_public_key_fp_sha256', // hint: ssh-kegen -lf id_rsa.pub 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", ...]
}; };

View File

@ -37,8 +37,8 @@ The following are ACS codes available as of this writing:
| MM<i>minutes</i> | It is currently >= _minutes_ past midnight (system time) | | MM<i>minutes</i> | It is currently >= _minutes_ past midnight (system time) |
| AC<i>achievementCount</i> | User has >= _achievementCount_ achievements | | AC<i>achievementCount</i> | User has >= _achievementCount_ achievements |
| AP<i>achievementPoints</i> | User has >= _achievementPoints_ achievement points | | AP<i>achievementPoints</i> | User has >= _achievementPoints_ achievement points |
| AF<i>authFactor</i> | User's current *Authentication Factor* is >= _authFactor_. Authentication factor 1 refers to username + password (or PubKey) while factor 2 refers to 2FA such as One-Time-Password authentication. |
\* Many more ACS codes are planned for the near future. | AR<i>authFactorReq</i> | Current users **requires** an Authentication Factor >= _authFactorReq_ |
## ACS Strings ## ACS Strings
ACS strings are one or more ACS codes in addition to some basic language semantics. ACS strings are one or more ACS codes in addition to some basic language semantics.

View File

@ -86,6 +86,7 @@ Many built in global/system methods exist. Below are a few. See [system_menu_met
| Method | Description | | Method | Description |
|--------|-------------| |--------|-------------|
| `login` | Performs a standard login. | | `login` | Performs a standard login. |
| `login2FA_OTP` | Performs a 2-Factor Authentication (2FA) One-Time Password (OTP) check, if configured for the user. |
| `logoff` | Performs a standard system logoff. | | `logoff` | Performs a standard system logoff. |
| `prevMenu` | Goes to the previous menu. | | `prevMenu` | Goes to the previous menu. |
| `nextMenu` | Goes to the next menu (as set by `next`) | | `nextMenu` | Goes to the next menu (as set by `next`) |

View File

@ -2,6 +2,7 @@
{ {
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const User = require('./user.js');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
@ -138,6 +139,22 @@
SC : function isSecureConnection() { SC : function isSecureConnection() {
return _.get(client, 'session.isSecure', false); 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() { ML : function minutesLeft() {
// :TODO: implement me! // :TODO: implement me!
return false; return false;

View File

@ -54,7 +54,8 @@
"uuid-parse": "^1.0.0", "uuid-parse": "^1.0.0",
"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"
}, },
"devDependencies": {}, "devDependencies": {},
"engines": { "engines": {

View File

@ -1478,6 +1478,13 @@ osenv@^0.1.4:
os-homedir "^1.0.0" os-homedir "^1.0.0"
os-tmpdir "^1.0.0" os-tmpdir "^1.0.0"
otplib@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/otplib/-/otplib-10.0.1.tgz#d37fcd13203298c0b94937d55c5a3527ed877875"
integrity sha512-FtbKelYtio2af5LDBWz3bWS6T03taHJAIv3evMrXuvoM50z5jbWoEMabPCk0A0JqiLGBzAIDJWfR9gSsvRYZHA==
dependencies:
thirty-two "1.0.2"
p-finally@^1.0.0: p-finally@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
@ -2026,6 +2033,11 @@ temptmp@^1.1.0:
dependencies: dependencies:
del "^3.0.0" del "^3.0.0"
thirty-two@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a"
integrity sha1-TKL//AKlEpDSdEueP1V2k8prYno=
through2@^2.0.3: through2@^2.0.3:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"