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);