Completed 2 step login support.

This commit is contained in:
Ylian Saint-Hilaire 2019-01-15 18:21:03 -08:00
parent faa3df958f
commit ff173b8788
5 changed files with 143 additions and 52 deletions

View File

@ -1342,58 +1342,72 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
}
case 'otpauth-request':
{
// Request a one time password to be setup
const otplib = require('otplib');
const secret = otplib.authenticator.generateSecret();
ws.send(JSON.stringify({ action: 'otpauth-request', secret: secret, url: otplib.authenticator.keyuri(user.name, 'MeshCentral', secret) }));
// Check is 2-step login is supported
const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.parent.certificates.CommonName != 'un-configured') && (obj.args.lanonly !== true) && (obj.args.nousers !== true));
if (twoStepLoginSupported) {
// Request a one time password to be setup
const otplib = require('otplib');
const secret = otplib.authenticator.generateSecret(); // TODO: Check the random source of this value.
ws.send(JSON.stringify({ action: 'otpauth-request', secret: secret, url: otplib.authenticator.keyuri(user.name, obj.parent.certificates.CommonName, secret) }));
}
break;
}
case 'otpauth-setup':
{
// Perform the one time password setup
if (require('otplib').authenticator.check(command.token, command.secret) === true) {
// Token is valid, activate 2-step login on this account.
user.otpsecret = command.secret;
obj.parent.db.SetUser(user);
ws.send(JSON.stringify({ action: 'otpauth-setup', success: true })); // Report success
// Check is 2-step login is supported
const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.parent.certificates.CommonName != 'un-configured') && (obj.args.lanonly !== true) && (obj.args.nousers !== true));
if (twoStepLoginSupported) {
// Perform the one time password setup
const otplib = require('otplib');
otplib.authenticator.options = { window: 6 }; // Set +/- 3 minute window
if (otplib.authenticator.check(command.token, command.secret) === true) {
// Token is valid, activate 2-step login on this account.
user.otpsecret = command.secret;
obj.parent.db.SetUser(user);
ws.send(JSON.stringify({ action: 'otpauth-setup', success: true })); // Report success
// Notify change
var userinfo = obj.common.Clone(user);
delete userinfo.hash;
delete userinfo.passhint;
delete userinfo.salt;
delete userinfo.type;
delete userinfo.domain;
delete userinfo.subscriptions;
delete userinfo.passtype;
if (userinfo.otpsecret) { userinfo.otpsecret = 1; }
try { ws.send(JSON.stringify({ action: 'userinfo', userinfo: userinfo })); } catch (ex) { }
} else {
ws.send(JSON.stringify({ action: 'otpauth-setup', success: false })); // Report fail
// Notify change
var userinfo = obj.common.Clone(user);
delete userinfo.hash;
delete userinfo.passhint;
delete userinfo.salt;
delete userinfo.type;
delete userinfo.domain;
delete userinfo.subscriptions;
delete userinfo.passtype;
if (userinfo.otpsecret) { userinfo.otpsecret = 1; }
try { ws.send(JSON.stringify({ action: 'userinfo', userinfo: userinfo })); } catch (ex) { }
} else {
ws.send(JSON.stringify({ action: 'otpauth-setup', success: false })); // Report fail
}
}
break;
}
case 'otpauth-clear':
{
// Clear the one time password secret
if (user.otpsecret) {
delete user.otpsecret;
obj.parent.db.SetUser(user);
// Check is 2-step login is supported
const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.parent.certificates.CommonName != 'un-configured') && (obj.args.lanonly !== true) && (obj.args.nousers !== true));
if (twoStepLoginSupported) {
// Clear the one time password secret
if (user.otpsecret) {
delete user.otpsecret;
obj.parent.db.SetUser(user);
// Notify change
var userinfo = obj.common.Clone(user);
delete userinfo.hash;
delete userinfo.passhint;
delete userinfo.salt;
delete userinfo.type;
delete userinfo.domain;
delete userinfo.subscriptions;
delete userinfo.passtype;
if (userinfo.otpsecret) { userinfo.otpsecret = 1; }
try { ws.send(JSON.stringify({ action: 'userinfo', userinfo: userinfo })); } catch (ex) { }
ws.send(JSON.stringify({ action: 'otpauth-clear', success: true })); // Report success
} else {
ws.send(JSON.stringify({ action: 'otpauth-clear', success: false })); // Report fail
// Notify change
var userinfo = obj.common.Clone(user);
delete userinfo.hash;
delete userinfo.passhint;
delete userinfo.salt;
delete userinfo.type;
delete userinfo.domain;
delete userinfo.subscriptions;
delete userinfo.passtype;
if (userinfo.otpsecret) { userinfo.otpsecret = 1; }
try { ws.send(JSON.stringify({ action: 'userinfo', userinfo: userinfo })); } catch (ex) { }
ws.send(JSON.stringify({ action: 'otpauth-clear', success: true })); // Report success
} else {
ws.send(JSON.stringify({ action: 'otpauth-clear', success: false })); // Report fail
}
}
break;
}

View File

@ -1121,10 +1121,8 @@
updateSiteAdmin();
QV('verifyEmailId', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true));
QV('verifyEmailId2', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true));
if ((features & 4096) != 0) {
QV('otpAuth', (userinfo.otpsecret != 1));
QV('otpAuthRemove', (userinfo.otpsecret == 1));
}
QV('otpAuth', ((features & 4096) != 0) && (userinfo.otpsecret != 1));
QV('otpAuthRemove', ((features & 4096) != 0) && (userinfo.otpsecret == 1));
break;
}
case 'users': {
@ -1315,7 +1313,10 @@
}
case 'otpauth-request': {
if ((xxdialogMode == 2) && (xxdialogTag == 'otpauth-request')) {
QH('d2optinfo', '<table style=width:380px><tr><td style=vertical-align:top>Install <a href=\"https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2\" rel=\"noreferrer noopener\" target=_blank>Google Authenticator</a> or a compatible application and scan the barcode, use <a href=\"' + message.url + '\" rel=\"noreferrer noopener\" target=_blank> this link</a> or enter the secret. Then, enter the current 6 digit token below to activate 2-Step login.<br /><br />Secret<br /><tt id=d2optsecret style=font-size:12px>' + message.secret + '</tt><br /><br /></td><td style=width:1px;vertical-align:top><a href=\"' + message.url + '\" rel=\"noreferrer noopener\" target=_blank><div id="qrcode"></div></a></td><tr><td colspan=2 style="text-align:center;border-top:1px solid black"><br />Enter the token here for 2-step login: <input type=text onkeypress=\"return (event.keyCode == 8) || (event.charCode >= 48 && event.charCode <= 57)\" onkeyup=account_addOtpCheck() onkeydown=account_addOtpCheck() maxlength=6 id=d2otpauthinput type="text"></td></table>');
var secret = message.secret;
if (secret.length == 52) { secret = secret.split(/(.............)/).filter(Boolean).join(' '); }
else if (secret.length == 32) { secret = secret.split(/(....)/).filter(Boolean).join(' '); secret = secret.substring(0, 20) + '<br/>' + secret.substring(20) }
QH('d2optinfo', '<table style=width:380px><tr><td style=vertical-align:top>Install <a href=\"https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2\" rel=\"noreferrer noopener\" target=_blank>Google Authenticator</a> or a compatible application and scan the barcode, use <a href=\"' + message.url + '\" rel=\"noreferrer noopener\" target=_blank> this link</a> or enter the secret. Then, enter the current 6 digit token below to activate 2-Step login.<br /><br />Secret<br /><tt id=d2optsecret secret=\"' + message.secret + '\" style=font-size:12px>' + secret + '</tt><br /><br /></td><td style=width:1px;vertical-align:top><a href=\"' + message.url + '\" rel=\"noreferrer noopener\" target=_blank><div id="qrcode"></div></a></td><tr><td colspan=2 style="text-align:center;border-top:1px solid black"><br />Enter the token here for 2-step login: <input type=text onkeypress=\"return (event.keyCode == 8) || (event.charCode >= 48 && event.charCode <= 57)\" onkeyup=account_addOtpCheck(event) onkeydown=account_addOtpCheck() maxlength=6 id=d2otpauthinput type=text></td></table>');
new QRCode(Q("qrcode"), { text: message.url, width: 128, height: 128, colorDark: "#000000", colorLight: "#EEE", correctLevel: QRCode.CorrectLevel.H });
QV('idx_dlgOkButton', true);
QE('idx_dlgOkButton', false);
@ -5056,12 +5057,14 @@
function account_addOtp() {
if (xxdialogMode || (userinfo.otpsecret == 1) || ((features & 4096) == 0)) return;
setDialogMode(2, "Add 2-Step Login", 2, function () { meshserver.send({ action: 'otpauth-setup', secret: Q('d2optsecret').innerHTML, token: Q('d2otpauthinput').value }); }, "<div id=d2optinfo>Loading...</div>", 'otpauth-request');
setDialogMode(2, "Add 2-Step Login", 2, function () { meshserver.send({ action: 'otpauth-setup', secret: Q('d2optsecret').attributes.secret.value, token: Q('d2otpauthinput').value }); }, "<div id=d2optinfo>Loading...</div>", 'otpauth-request');
meshserver.send({ action: 'otpauth-request' });
}
function account_addOtpCheck() {
QE('idx_dlgOkButton', Q('d2otpauthinput').value.length == 6);
function account_addOtpCheck(e) {
const v = (Q('d2otpauthinput').value.length == 6);
QE('idx_dlgOkButton', v);
if (e && (e.keyCode == 13) && v) { dialogclose(1); }
}
function account_removeOtp() {

View File

@ -117,7 +117,7 @@
</form>
</div>
</div>
<div id=resetpanel style="background-color: #979797;border-radius:16px;width:260px;padding:16px;text-align:center;display:none;clear:both">
<div id=resetpanel style="background-color:#979797;border-radius:16px;width:260px;padding:16px;text-align:center;display:none;clear:both">
<form action=resetaccount method=post>
<div id=message3>
{{{message}}}
@ -140,6 +140,25 @@
<hr /><a onclick=xgo(1) style=cursor:pointer>Back to login</a>
</form>
</div>
<div id=tokenpanel style="background-color:#979797;border-radius:16px;width:260px;padding:16px;text-align:center;display:none;clear:both">
<form action=tokenlogin method=post autocomplete=off>
<div id=message4>
{{{message}}}
</div>
<table>
<tr>
<td align=right width=100>Login token:</td>
<td><input id=tokenInput type=text name=token maxlength=6 onkeypress="return (event.keyCode == 8) || (event.keyCode == 13) || (event.charCode >= 48 && event.charCode <= 57)" onkeyup=checkToken(event) onkeydown=checkToken(event) /></td>
</tr>
<tr>
<td colspan=2>
<div style=float:right><input id=tokenOkButton type=submit value="Login" disabled="disabled" /></div>
</td>
</tr>
</table>
<hr /><a onclick=xgo(1) style=cursor:pointer>Back to login</a>
</form>
</div>
</td>
</tr>
</table>
@ -218,9 +237,11 @@
QV('loginpanel', x == 1);
QV('createpanel', x == 2);
QV('resetpanel', x == 3);
QV('tokenpanel', x == 4);
if (x == 1) { Q('username').focus(); }
if (x == 2) { Q('ausername').focus(); }
if (x == 3) { Q('remail').focus(); }
if (x == 4) { Q('tokenInput').focus(); }
}
function validateLogin(box, e) {
@ -307,6 +328,8 @@
return true;
}
function checkToken() { QE('tokenOkButton', Q('tokenInput').value.length == 6); }
//
// POPUP DIALOG
//

View File

@ -213,6 +213,25 @@
<hr /><a onclick=xgo(1) style=cursor:pointer>Back to login</a>
</form>
</div>
<div id=tokenpanel style="background-color: #979797;border-radius:16px;width:300px;padding:16px;text-align:center;display:none">
<form action=tokenlogin method=post autocomplete=off>
<div id=message4>
{{{message}}}
</div>
<table>
<tr>
<td align=right width=100>Login token:</td>
<td><input id=tokenInput type=text name=token maxlength=6 onkeypress="return (event.keyCode == 8) || (event.keyCode == 13) || (event.charCode >= 48 && event.charCode <= 57)" onkeyup=checkToken(event) onkeydown=checkToken(event) /></td>
</tr>
<tr>
<td colspan=2>
<div style=float:right><input id=tokenOkButton type=submit value="Login" disabled="disabled" /></div>
</td>
</tr>
</table>
<hr /><a onclick=xgo(1) style=cursor:pointer>Back to login</a>
</form>
</div>
</td>
</tr>
</table>
@ -301,9 +320,11 @@
QV('loginpanel', x == 1);
QV('createpanel', x == 2);
QV('resetpanel', x == 3);
QV('tokenpanel', x == 4);
if (x == 1) { Q('username').focus(); }
if (x == 2) { Q('ausername').focus(); }
if (x == 3) { Q('remail').focus(); }
if (x == 4) { Q('tokenInput').focus(); }
}
function validateLogin(box, e) {
@ -402,6 +423,8 @@
return true;
}
function checkToken() { QE('tokenOkButton', Q('tokenInput').value.length == 6); }
//
// POPUP DIALOG
//

View File

@ -241,6 +241,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
// Authenticate the user
obj.authenticate = function (name, pass, domain, fn) {
if ((typeof (name) != 'string') || (typeof (pass) != 'string') || (typeof (domain) != 'object')) { fn(new Error('invalid fields')); return; }
if (!module.parent) console.log('authenticating %s:%s:%s', domain.id, name, pass);
var user = obj.users['user/' + domain.id + '/' + name.toLowerCase()];
// Query the db for the given username
@ -346,10 +347,31 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
function handleLoginRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) return;
obj.authenticate(req.body.username, req.body.password, domain, function (err, userid, passhint) {
// Normally, use the body username/password. If this is a token, use the username/password in the session.
var xusername = req.body.username, xpassword = req.body.password;
if ((xusername == null) && (xpassword == null) && (req.body.token != null)) { xusername = req.session.tokenusername; xpassword = req.session.tokenpassword; }
// Authenticate the user
obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint) {
if (userid) {
var user = obj.users[userid];
// Check if this user has 2-step login active
var tokenValid = 0;
const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName != 'un-configured') && (obj.args.lanonly !== true) && (obj.args.nousers !== true));
const otplib = require('otplib')
otplib.authenticator.options = { window: 6 }; // Set +/- 3 minute window
if (twoStepLoginSupported && user.otpsecret && ((typeof (req.body.token) != 'string') || ((tokenValid = otplib.authenticator.check(req.body.token, user.otpsecret)) !== true))) {
// 2-step auth is required, but the token is not present or not valid.
if (tokenValid === false) { req.session.error = '<b style=color:#8C001A>Invalid token, try again.</b>'; }
req.session.loginmode = '4';
req.session.tokenusername = xusername;
req.session.tokenpassword = xpassword;
res.redirect(domain.url);
return;
}
// Save login time
user.login = Date.now();
obj.db.SetUser(user);
@ -359,6 +381,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
// Store the user's primary key in the session store to be retrieved, or in this case the entire user object
// req.session.success = 'Authenticated as ' + user.name + 'click to <a href="/logout">logout</a>. You may now access <a href="/restricted">/restricted</a>.';
delete req.session.loginmode;
delete req.session.tokenusername;
delete req.session.tokenpassword;
req.session.userid = userid;
req.session.domainid = domain.id;
req.session.currentNode = '';
@ -526,6 +550,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
delete userinfo.domain;
delete userinfo.subscriptions;
delete userinfo.passtype;
if (userinfo.otpsecret) { userinfo.otpsecret = 1; }
obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, { etype: 'user', username: userinfo.name, account: userinfo, action: 'accountchange', msg: 'Verified email of user ' + EscapeHtml(user.name) + ' (' + EscapeHtml(userinfo.email) + ')', domain: domain.id });
// Send the confirmation page
@ -554,6 +579,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
userinfo.hash = hash;
userinfo.passchange = Date.now();
userinfo.passhint = null;
delete userinfo.otpsecret; // Currently a email password reset will turn off 2-step login.
obj.db.SetUser(userinfo);
// Event the change
@ -565,6 +591,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
delete userinfo.domain;
delete userinfo.subscriptions;
delete userinfo.passtype;
if (userinfo.otpsecret) { userinfo.otpsecret = 1; }
obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, { etype: 'user', username: userinfo.name, account: userinfo, action: 'accountchange', msg: 'Password reset for user ' + EscapeHtml(user.name), domain: domain.id });
// Send the new password
@ -780,7 +807,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if (obj.args.allowhighqualitydesktop == true) { features += 512; } // Enable AllowHighQualityDesktop (Default false)
if (obj.args.lanonly == true || obj.args.mpsport == 0) { features += 1024; } // No CIRA
if ((obj.parent.serverSelfWriteAllowed == true) && (user != null) && (user.siteadmin == 0xFFFFFFFF)) { features += 2048; } // Server can self-write (Allows self-update)
if (domain.auth != 'sspi') { features += 4096; } // Two-factor auth supported
if ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName != 'un-configured') && (obj.args.lanonly !== true) && (obj.args.nousers !== true)) { features += 4096; } // 2-step login supported
// Send the master web application
if ((!obj.args.user) && (obj.args.nousers != true) && (nologout == false)) { logoutcontrol += ' <a href=' + domain.url + 'logout?' + Math.random() + ' style=color:white>Logout</a>'; } // If a default user is in use or no user mode, don't display the logout button
@ -1883,6 +1910,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
obj.app.get(url, handleRootRequest);
obj.app.get(url + 'terms', handleTermsRequest);
obj.app.post(url + 'login', handleLoginRequest);
obj.app.post(url + 'tokenlogin', handleLoginRequest);
obj.app.get(url + 'logout', handleLogoutRequest);
obj.app.get(url + 'MeshServerRootCert.cer', handleRootCertRequest);
obj.app.get(url + 'mescript.ashx', handleMeScriptRequest);