Completed 2 step login support.
Ylian Saint-Hilaire committed
Jan 15, 2019 at 18:21 UTC
14f5c33ef3665a9d1c7179c27487f1b1382f5ec7
5 files changed
+145
-54
meshuser.js
+57
-43
@@ -1342,58 +1342,72 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
1342
}
1343
case 'otpauth-request':
1344
{
1345
- // Request a one time password to be setup
1346
- const otplib = require('otplib');
1347
- const secret = otplib.authenticator.generateSecret();
1348
- ws.send(JSON.stringify({ action: 'otpauth-request', secret: secret, url: otplib.authenticator.keyuri(user.name, 'MeshCentral', secret) }));
1345
+ // Check is 2-step login is supported
1346
+ const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.parent.certificates.CommonName != 'un-configured') && (obj.args.lanonly !== true) && (obj.args.nousers !== true));
1347
+ if (twoStepLoginSupported) {
1348
+ // Request a one time password to be setup
1349
+ const otplib = require('otplib');
1350
+ const secret = otplib.authenticator.generateSecret(); // TODO: Check the random source of this value.
1351
+ ws.send(JSON.stringify({ action: 'otpauth-request', secret: secret, url: otplib.authenticator.keyuri(user.name, obj.parent.certificates.CommonName, secret) }));
1352
+ }
1353
break;
1354
}
1355
case 'otpauth-setup':
1356
{
1353
- // Perform the one time password setup
1354
- if (require('otplib').authenticator.check(command.token, command.secret) === true) {
1355
- // Token is valid, activate 2-step login on this account.
1356
- user.otpsecret = command.secret;
1357
- obj.parent.db.SetUser(user);
1358
- ws.send(JSON.stringify({ action: 'otpauth-setup', success: true })); // Report success
1359
-
1360
- // Notify change
1361
- var userinfo = obj.common.Clone(user);
1362
- delete userinfo.hash;
1363
- delete userinfo.passhint;
1364
- delete userinfo.salt;
1365
- delete userinfo.type;
1366
- delete userinfo.domain;
1367
- delete userinfo.subscriptions;
1368
- delete userinfo.passtype;
1369
- if (userinfo.otpsecret) { userinfo.otpsecret = 1; }
1370
- try { ws.send(JSON.stringify({ action: 'userinfo', userinfo: userinfo })); } catch (ex) { }
1371
- } else {
1372
- ws.send(JSON.stringify({ action: 'otpauth-setup', success: false })); // Report fail
1357
+ // Check is 2-step login is supported
1358
+ const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.parent.certificates.CommonName != 'un-configured') && (obj.args.lanonly !== true) && (obj.args.nousers !== true));
1359
+ if (twoStepLoginSupported) {
1360
+ // Perform the one time password setup
1361
+ const otplib = require('otplib');
1362
+ otplib.authenticator.options = { window: 6 }; // Set +/- 3 minute window
1363
+ if (otplib.authenticator.check(command.token, command.secret) === true) {
1364
+ // Token is valid, activate 2-step login on this account.
1365
+ user.otpsecret = command.secret;
1366
+ obj.parent.db.SetUser(user);
1367
+ ws.send(JSON.stringify({ action: 'otpauth-setup', success: true })); // Report success
1368
+
1369
+ // Notify change
1370
+ var userinfo = obj.common.Clone(user);
1371
+ delete userinfo.hash;
1372
+ delete userinfo.passhint;
1373
+ delete userinfo.salt;
1374
+ delete userinfo.type;
1375
+ delete userinfo.domain;
1376
+ delete userinfo.subscriptions;
1377
+ delete userinfo.passtype;
1378
+ if (userinfo.otpsecret) { userinfo.otpsecret = 1; }
1379
+ try { ws.send(JSON.stringify({ action: 'userinfo', userinfo: userinfo })); } catch (ex) { }
1380
+ } else {
1381
+ ws.send(JSON.stringify({ action: 'otpauth-setup', success: false })); // Report fail
1382
+ }
1383
}
1384
break;
1385
}
1386
case 'otpauth-clear':
1387
{
1378
- // Clear the one time password secret
1379
- if (user.otpsecret) {
1380
- delete user.otpsecret;
1381
- obj.parent.db.SetUser(user);
1382
-
1383
- // Notify change
1384
- var userinfo = obj.common.Clone(user);
1385
- delete userinfo.hash;
1386
- delete userinfo.passhint;
1387
- delete userinfo.salt;
1388
- delete userinfo.type;
1389
- delete userinfo.domain;
1390
- delete userinfo.subscriptions;
1391
- delete userinfo.passtype;
1392
- if (userinfo.otpsecret) { userinfo.otpsecret = 1; }
1393
- try { ws.send(JSON.stringify({ action: 'userinfo', userinfo: userinfo })); } catch (ex) { }
1394
- ws.send(JSON.stringify({ action: 'otpauth-clear', success: true })); // Report success
1395
- } else {
1396
- ws.send(JSON.stringify({ action: 'otpauth-clear', success: false })); // Report fail
1388
+ // Check is 2-step login is supported
1389
+ const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.parent.certificates.CommonName != 'un-configured') && (obj.args.lanonly !== true) && (obj.args.nousers !== true));
1390
+ if (twoStepLoginSupported) {
1391
+ // Clear the one time password secret
1392
+ if (user.otpsecret) {
1393
+ delete user.otpsecret;
1394
+ obj.parent.db.SetUser(user);
1395
+
1396
+ // Notify change
1397
+ var userinfo = obj.common.Clone(user);
1398
+ delete userinfo.hash;
1399
+ delete userinfo.passhint;
1400
+ delete userinfo.salt;
1401
+ delete userinfo.type;
1402
+ delete userinfo.domain;
1403
+ delete userinfo.subscriptions;
1404
+ delete userinfo.passtype;
1405
+ if (userinfo.otpsecret) { userinfo.otpsecret = 1; }
1406
+ try { ws.send(JSON.stringify({ action: 'userinfo', userinfo: userinfo })); } catch (ex) { }
1407
+ ws.send(JSON.stringify({ action: 'otpauth-clear', success: true })); // Report success
1408
+ } else {
1409
+ ws.send(JSON.stringify({ action: 'otpauth-clear', success: false })); // Report fail
1410
+ }
1411
}
1412
break;
1413
}
views/default.handlebars
+11
-8
@@ -1121,10 +1121,8 @@
1121
updateSiteAdmin();
1122
QV('verifyEmailId', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true));
1123
QV('verifyEmailId2', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true));
1124
- if ((features & 4096) != 0) {
1125
- QV('otpAuth', (userinfo.otpsecret != 1));
1126
- QV('otpAuthRemove', (userinfo.otpsecret == 1));
1127
- }
1124
+ QV('otpAuth', ((features & 4096) != 0) && (userinfo.otpsecret != 1));
1125
+ QV('otpAuthRemove', ((features & 4096) != 0) && (userinfo.otpsecret == 1));
1126
break;
1127
}
1128
case 'users': {
@@ -1315,7 +1313,10 @@
1313
}
1314
case 'otpauth-request': {
1315
if ((xxdialogMode == 2) && (xxdialogTag == 'otpauth-request')) {
1318
- 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>');
1316
+ var secret = message.secret;
1317
+ if (secret.length == 52) { secret = secret.split(/(.............)/).filter(Boolean).join(' '); }
1318
+ else if (secret.length == 32) { secret = secret.split(/(....)/).filter(Boolean).join(' '); secret = secret.substring(0, 20) + '<br/>' + secret.substring(20) }
1319
+ 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>');
1320
new QRCode(Q("qrcode"), { text: message.url, width: 128, height: 128, colorDark: "#000000", colorLight: "#EEE", correctLevel: QRCode.CorrectLevel.H });
1321
QV('idx_dlgOkButton', true);
1322
QE('idx_dlgOkButton', false);
@@ -5056,12 +5057,14 @@
5057
5058
function account_addOtp() {
5059
if (xxdialogMode || (userinfo.otpsecret == 1) || ((features & 4096) == 0)) return;
5059
- 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');
5060
+ 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');
5061
meshserver.send({ action: 'otpauth-request' });
5062
}
5063
5063
- function account_addOtpCheck() {
5064
- QE('idx_dlgOkButton', Q('d2otpauthinput').value.length == 6);
5064
+ function account_addOtpCheck(e) {
5065
+ const v = (Q('d2otpauthinput').value.length == 6);
5066
+ QE('idx_dlgOkButton', v);
5067
+ if (e && (e.keyCode == 13) && v) { dialogclose(1); }
5068
}
5069
5070
function account_removeOtp() {
views/login-mobile.handlebars
+24
-1
@@ -117,7 +117,7 @@
117
</form>
118
</div>
119
</div>
120
- <div id=resetpanel style="background-color: #979797;border-radius:16px;width:260px;padding:16px;text-align:center;display:none;clear:both">
120
+ <div id=resetpanel style="background-color:#979797;border-radius:16px;width:260px;padding:16px;text-align:center;display:none;clear:both">
121
<form action=resetaccount method=post>
122
<div id=message3>
123
{{{message}}}
@@ -140,6 +140,25 @@
140
<hr /><a onclick=xgo(1) style=cursor:pointer>Back to login</a>
141
</form>
142
</div>
143
+ <div id=tokenpanel style="background-color:#979797;border-radius:16px;width:260px;padding:16px;text-align:center;display:none;clear:both">
144
+ <form action=tokenlogin method=post autocomplete=off>
145
+ <div id=message4>
146
+ {{{message}}}
147
+ </div>
148
+ <table>
149
+ <tr>
150
+ <td align=right width=100>Login token:</td>
151
+ <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>
152
+ </tr>
153
+ <tr>
154
+ <td colspan=2>
155
+ <div style=float:right><input id=tokenOkButton type=submit value="Login" disabled="disabled" /></div>
156
+ </td>
157
+ </tr>
158
+ </table>
159
+ <hr /><a onclick=xgo(1) style=cursor:pointer>Back to login</a>
160
+ </form>
161
+ </div>
162
</td>
163
</tr>
164
</table>
@@ -218,9 +237,11 @@
237
QV('loginpanel', x == 1);
238
QV('createpanel', x == 2);
239
QV('resetpanel', x == 3);
240
+ QV('tokenpanel', x == 4);
241
if (x == 1) { Q('username').focus(); }
242
if (x == 2) { Q('ausername').focus(); }
243
if (x == 3) { Q('remail').focus(); }
244
+ if (x == 4) { Q('tokenInput').focus(); }
245
}
246
247
function validateLogin(box, e) {
@@ -307,6 +328,8 @@
328
return true;
329
}
330
331
+ function checkToken() { QE('tokenOkButton', Q('tokenInput').value.length == 6); }
332
+
333
//
334
// POPUP DIALOG
335
//
views/login.handlebars
+23
@@ -213,6 +213,25 @@
213
<hr /><a onclick=xgo(1) style=cursor:pointer>Back to login</a>
214
</form>
215
</div>
216
+ <div id=tokenpanel style="background-color: #979797;border-radius:16px;width:300px;padding:16px;text-align:center;display:none">
217
+ <form action=tokenlogin method=post autocomplete=off>
218
+ <div id=message4>
219
+ {{{message}}}
220
+ </div>
221
+ <table>
222
+ <tr>
223
+ <td align=right width=100>Login token:</td>
224
+ <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>
225
+ </tr>
226
+ <tr>
227
+ <td colspan=2>
228
+ <div style=float:right><input id=tokenOkButton type=submit value="Login" disabled="disabled" /></div>
229
+ </td>
230
+ </tr>
231
+ </table>
232
+ <hr /><a onclick=xgo(1) style=cursor:pointer>Back to login</a>
233
+ </form>
234
+ </div>
235
</td>
236
</tr>
237
</table>
@@ -301,9 +320,11 @@
320
QV('loginpanel', x == 1);
321
QV('createpanel', x == 2);
322
QV('resetpanel', x == 3);
323
+ QV('tokenpanel', x == 4);
324
if (x == 1) { Q('username').focus(); }
325
if (x == 2) { Q('ausername').focus(); }
326
if (x == 3) { Q('remail').focus(); }
327
+ if (x == 4) { Q('tokenInput').focus(); }
328
}
329
330
function validateLogin(box, e) {
@@ -402,6 +423,8 @@
423
return true;
424
}
425
426
+ function checkToken() { QE('tokenOkButton', Q('tokenInput').value.length == 6); }
427
+
428
//
429
// POPUP DIALOG
430
//
webserver.js
+30
-2
@@ -241,6 +241,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
241
242
// Authenticate the user
243
obj.authenticate = function (name, pass, domain, fn) {
244
+ if ((typeof (name) != 'string') || (typeof (pass) != 'string') || (typeof (domain) != 'object')) { fn(new Error('invalid fields')); return; }
245
if (!module.parent) console.log('authenticating %s:%s:%s', domain.id, name, pass);
246
var user = obj.users['user/' + domain.id + '/' + name.toLowerCase()];
247
// Query the db for the given username
@@ -346,10 +347,31 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
347
function handleLoginRequest(req, res) {
348
const domain = checkUserIpAddress(req, res);
349
if (domain == null) return;
349
- obj.authenticate(req.body.username, req.body.password, domain, function (err, userid, passhint) {
350
+
351
+ // Normally, use the body username/password. If this is a token, use the username/password in the session.
352
+ var xusername = req.body.username, xpassword = req.body.password;
353
+ if ((xusername == null) && (xpassword == null) && (req.body.token != null)) { xusername = req.session.tokenusername; xpassword = req.session.tokenpassword; }
354
+
355
+ // Authenticate the user
356
+ obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint) {
357
if (userid) {
358
var user = obj.users[userid];
359
360
+ // Check if this user has 2-step login active
361
+ var tokenValid = 0;
362
+ const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName != 'un-configured') && (obj.args.lanonly !== true) && (obj.args.nousers !== true));
363
+ const otplib = require('otplib')
364
+ otplib.authenticator.options = { window: 6 }; // Set +/- 3 minute window
365
+ if (twoStepLoginSupported && user.otpsecret && ((typeof (req.body.token) != 'string') || ((tokenValid = otplib.authenticator.check(req.body.token, user.otpsecret)) !== true))) {
366
+ // 2-step auth is required, but the token is not present or not valid.
367
+ if (tokenValid === false) { req.session.error = '<b style=color:#8C001A>Invalid token, try again.</b>'; }
368
+ req.session.loginmode = '4';
369
+ req.session.tokenusername = xusername;
370
+ req.session.tokenpassword = xpassword;
371
+ res.redirect(domain.url);
372
+ return;
373
+ }
374
+
375
// Save login time
376
user.login = Date.now();
377
obj.db.SetUser(user);
@@ -359,6 +381,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
381
// Store the user's primary key in the session store to be retrieved, or in this case the entire user object
382
// req.session.success = 'Authenticated as ' + user.name + 'click to <a href="/logout">logout</a>. You may now access <a href="/restricted">/restricted</a>.';
383
delete req.session.loginmode;
384
+ delete req.session.tokenusername;
385
+ delete req.session.tokenpassword;
386
req.session.userid = userid;
387
req.session.domainid = domain.id;
388
req.session.currentNode = '';
@@ -526,6 +550,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
550
delete userinfo.domain;
551
delete userinfo.subscriptions;
552
delete userinfo.passtype;
553
+ if (userinfo.otpsecret) { userinfo.otpsecret = 1; }
554
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 });
555
556
// Send the confirmation page
@@ -554,6 +579,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
579
userinfo.hash = hash;
580
userinfo.passchange = Date.now();
581
userinfo.passhint = null;
582
+ delete userinfo.otpsecret; // Currently a email password reset will turn off 2-step login.
583
obj.db.SetUser(userinfo);
584
585
// Event the change
@@ -565,6 +591,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
591
delete userinfo.domain;
592
delete userinfo.subscriptions;
593
delete userinfo.passtype;
594
+ if (userinfo.otpsecret) { userinfo.otpsecret = 1; }
595
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 });
596
597
// Send the new password
@@ -780,7 +807,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
807
if (obj.args.allowhighqualitydesktop == true) { features += 512; } // Enable AllowHighQualityDesktop (Default false)
808
if (obj.args.lanonly == true || obj.args.mpsport == 0) { features += 1024; } // No CIRA
809
if ((obj.parent.serverSelfWriteAllowed == true) && (user != null) && (user.siteadmin == 0xFFFFFFFF)) { features += 2048; } // Server can self-write (Allows self-update)
783
- if (domain.auth != 'sspi') { features += 4096; } // Two-factor auth supported
810
+ if ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName != 'un-configured') && (obj.args.lanonly !== true) && (obj.args.nousers !== true)) { features += 4096; } // 2-step login supported
811
812
// Send the master web application
813
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) {
1910
obj.app.get(url, handleRootRequest);
1911
obj.app.get(url + 'terms', handleTermsRequest);
1912
obj.app.post(url + 'login', handleLoginRequest);
1913
+ obj.app.post(url + 'tokenlogin', handleLoginRequest);
1914
obj.app.get(url + 'logout', handleLogoutRequest);
1915
obj.app.get(url + 'MeshServerRootCert.cer', handleRootCertRequest);
1916
obj.app.get(url + 'mescript.ashx', handleMeScriptRequest);