mirror of
https://github.com/Ylianst/MeshCentral.git
synced 2024-12-23 21:55:52 -05:00
Completed 2 step login support.
This commit is contained in:
parent
bd693123e8
commit
14f5c33ef3
96
meshuser.js
96
meshuser.js
@ -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;
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
//
|
||||
|
@ -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
|
||||
//
|
||||
|
32
webserver.js
32
webserver.js
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user