From 65e6c1925c415cfd2a78bc17b85803bb3f414936 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Wed, 27 Feb 2019 18:48:50 -0800 Subject: [PATCH] Added server password timeout and reset on next login. --- agents/meshcmd.js | 8 ++ agents/meshinstall-linux.sh | 2 +- meshuser.js | 42 ++++++++-- sample-config.json | 2 +- swarmserver.js | 32 +++++-- views/default-min.handlebars | 2 +- views/default-mobile-min.handlebars | 2 +- views/default-mobile.handlebars | 15 +++- views/default.handlebars | 24 +++++- views/login-min.handlebars | 2 +- views/login-mobile-min.handlebars | 2 +- views/login-mobile.handlebars | 79 +++++++++++++++++ views/login.handlebars | 87 ++++++++++++++++++- webserver.js | 126 +++++++++++++++++++++++----- 14 files changed, 377 insertions(+), 48 deletions(-) diff --git a/agents/meshcmd.js b/agents/meshcmd.js index 4ba074ac..4694855c 100644 --- a/agents/meshcmd.js +++ b/agents/meshcmd.js @@ -316,6 +316,7 @@ function run(argv) { // Display Intel AMT versions var amtMeiModule = require('amt-mei'); var amtMei = new amtMeiModule(); + if (amtMei == null) { console.log("Intel(R) AMT not supported or insufficient access rights."); exit(1); return; } amtMei.on('error', function (e) { console.log('ERROR: ' + e); exit(1); return; }); amtMei.getVersion(function (val) { console.log("MEI Version = " + val.BiosVersion.toString()); @@ -326,6 +327,7 @@ function run(argv) { // Display Intel AMT list of trusted hashes var amtMeiModule = require('amt-mei'); var amtMei = new amtMeiModule(); + if (amtMei == null) { console.log("Intel(R) AMT not supported or insufficient access rights."); exit(1); return; } amtMei.on('error', function (e) { console.log('ERROR: ' + e); exit(1); return; }); amtMei.getHashHandles(function (handles) { exitOnCount = handles.length; @@ -341,6 +343,7 @@ function run(argv) { mestate = {}; var amtMeiModule = require('amt-mei'); var amtMei = new amtMeiModule(); + if (amtMei == null) { console.log("Intel(R) AMT not supported or insufficient access rights."); exit(1); return; } amtMei.on('error', function (e) { console.log('ERROR: ' + e); exit(1); return; }); amtMei.getVersion(function (result) { if (result) { for (var version in result.Versions) { if (result.Versions[version].Description == 'AMT') { mestate.ver = result.Versions[version].Version; } } } }); amtMei.getProvisioningState(function (result) { if (result) { mestate.ProvisioningState = result; } }); @@ -381,6 +384,7 @@ function run(argv) { mestate = {}; var amtMeiModule = require('amt-mei'); var amtMei = new amtMeiModule(); + if (amtMei == null) { console.log("Intel(R) AMT not supported or insufficient access rights."); exit(1); return; } amtMei.on('error', function (e) { console.log('ERROR: ' + e); exit(1); return; }); amtMei.getVersion(function (result) { console.log('getVersion: ' + JSON.stringify(result)); }); amtMei.getProvisioningState(function (result) { console.log('getProvisioningState: ' + JSON.stringify(result)); }); @@ -666,6 +670,7 @@ function startMeshCommander() { function deactivateCCM() { var amtMeiModule = require('amt-mei'); var amtMei = new amtMeiModule(); + if (amtMei == null) { console.log("Intel(R) AMT not supported or insufficient access rights."); exit(1); return; } amtMei.on('error', function (e) { console.log('ERROR: ' + e); exit(1); return; }); amtMei.unprovision(1, function (status) { if (status == 0) { console.log('Success'); } else { console.log('Error ' + status); } exit(1); }); } @@ -708,6 +713,7 @@ function getAmtUuid() { if (settings.hostname == null) { var amtMeiModule = require('amt-mei'); var amtMei = new amtMeiModule(); + if (amtMei == null) { console.log("Intel(R) AMT not supported or insufficient access rights."); exit(1); return; } amtMei.on('error', function (e) { console.log('ERROR: ' + e); exit(1); return; }); amtMei.getUuid(function (result) { if ((result == null) || (result.uuid == null)) { console.log('Failed.'); } else { console.log(result.uuid); } exit(1); }); } else { @@ -829,6 +835,7 @@ function getAmtInfo(func, tag) { getAmtInfoFetchingTimer = null; var amtMeiModule = require('amt-mei'); amtMei = new amtMeiModule(); + if (amtMei == null) { console.log("Intel(R) AMT not supported or insufficient access rights."); exit(1); return; } amtMei.on('error', function (e) { console.log('ERROR: ' + e); exit(1); return; }); }, 3000); amtMei.getProtocolVersion(function (result) { if (result != null) { amtMeiTmpState.MeiVersion = result; } }); @@ -890,6 +897,7 @@ function startLms(func) { var amtMeiModule = require('amt-mei'); amtMei = new amtMeiModule(); + if (amtMei == null) { console.log("Intel(R) AMT not supported or insufficient access rights."); exit(1); return; } amtMei.on('error', function (e) { console.log('ERROR: ' + e); exit(1); return; }); //console.log("PTHI Connected."); diff --git a/agents/meshinstall-linux.sh b/agents/meshinstall-linux.sh index 2a89bcf2..86cfbb18 100644 --- a/agents/meshinstall-linux.sh +++ b/agents/meshinstall-linux.sh @@ -58,7 +58,7 @@ CheckInstallAgent() { # Linux x86, 64 bit machineid=6 fi - if [ $machinetype == 'x86' ] || [ $machinetype == 'i686' ] + if [ $machinetype == 'x86' ] || [ $machinetype == 'i686' ] || [ $machinetype == 'i586' ] then # Linux x86, 32 bit machineid=5 diff --git a/meshuser.js b/meshuser.js index a5b7197f..2a07260f 100644 --- a/meshuser.js +++ b/meshuser.js @@ -470,7 +470,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use switch (cmd) { case 'help': { - r = 'Available commands: help, args, resetserver, showconfig, usersessions, tasklimiter, setmaxtasks, cores.'; + r = 'Available commands: help, args, resetserver, showconfig, usersessions, tasklimiter, setmaxtasks, cores, migrationagents, swarmstats.'; break; } case 'args': { @@ -536,6 +536,31 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use r = JSON.stringify(removeAllUnderScore(config), null, 4); break; } + case 'migrationagents': { + if (obj.parent.parent.swarmserver == null) { + r = 'Swarm server not running.'; + } else { + for (var i in obj.parent.parent.swarmserver.migrationAgents) { + var arch = obj.parent.parent.swarmserver.migrationAgents[i]; + for (var j in arch) { var agent = arch[j]; r += 'Arch ' + agent.arch + ', Ver ' + agent.ver + ', Size ' + agent.binary.length + '
'; } + } + } + break; + } + case 'swarmstats': { + if (obj.parent.parent.swarmserver == null) { + r = 'Swarm server not running.'; + } else { + for (var i in obj.parent.parent.swarmserver.stats) { + if (typeof obj.parent.parent.swarmserver.stats[i] == 'object') { + r += i + ' ' + JSON.stringify(obj.parent.parent.swarmserver.stats[i]) + '
'; + } else { + r += i + ' ' + obj.parent.parent.swarmserver.stats[i] + '
'; + } + } + } + break; + } default: { // This is an unknown command, return an error message r = 'Unknown command \"' + cmd + '\", type \"help\" for list of avaialble commands.'; break; @@ -822,11 +847,11 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (hint.length > 250) hint = hint.substring(0, 250); user.salt = salt; user.hash = hash; - user.passhint = req.body.apasswordhint; + user.passhint = hint; user.passchange = Math.floor(Date.now() / 1000); delete user.passtype; obj.db.SetUser(user); - obj.parent.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', username: user.name, action: 'passchange', msg: 'Account password changed: ' + user.name, domain: domain.id }); + obj.parent.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', username: user.name, account: obj.parent.CloneSafeUser(user), action: 'accountchange', msg: 'Account password changed: ' + user.name, domain: domain.id }); // Send user notification of password change displayNotificationMessage('Password changed.'); @@ -854,19 +879,18 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Compute the password hash & save it require('./pass').hash(command.pass, function (err, salt, hash) { if (!err) { - var annonceChange = false; chguser.salt = salt; chguser.hash = hash; chguser.passhint = command.hint; - chguser.passchange = Math.floor(Date.now() / 1000); + if (command.resetNextLogin == true) { chguser.passchange = -1; } else { chguser.passchange = Math.floor(Date.now() / 1000); } delete chguser.passtype; // Remove the password type if one was present. if (command.removeMultiFactor == true) { - if (chguser.otpsecret) { delete chguser.otpsecret; annonceChange = true; } - if (chguser.otphkeys) { delete chguser.otphkeys; annonceChange = true; } - if (chguser.otpkeys) { delete chguser.otpkeys; annonceChange = true; } + if (chguser.otpsecret) { delete chguser.otpsecret; } + if (chguser.otphkeys) { delete chguser.otphkeys; } + if (chguser.otpkeys) { delete chguser.otpkeys; } } obj.db.SetUser(chguser); - if (annonceChange == true) { obj.parent.parent.DispatchEvent(['*', 'server-users', user._id, chguser._id], obj, { etype: 'user', username: user.name, account: obj.parent.CloneSafeUser(chguser), action: 'accountchange', msg: 'Removed 2nd factor auth.', domain: domain.id }); } + obj.parent.parent.DispatchEvent(['*', 'server-users', user._id, chguser._id], obj, { etype: 'user', username: user.name, account: obj.parent.CloneSafeUser(chguser), action: 'accountchange', msg: 'Changed account credentials.', domain: domain.id }); } else { // Report that the password change failed // TODO diff --git a/sample-config.json b/sample-config.json index 55b21d57..8c8eb1ca 100644 --- a/sample-config.json +++ b/sample-config.json @@ -48,7 +48,7 @@ "_NewAccountEmailDomains": [ "sample.com" ], "Footer": "Twitter", "_CertUrl": "https://192.168.2.106:443/", - "_PasswordRequirements": { "min": 8, "max": 128, "upper": 1, "lower": 1, "numeric": 1, "nonalpha": 1 }, + "_PasswordRequirements": { "min": 8, "max": 128, "upper": 1, "lower": 1, "numeric": 1, "nonalpha": 1, "reset": 90 }, "_AgentNoProxy": true, "_GeoLocation": true, "_UserAllowedIP": "127.0.0.1,192.168.1.0/24", diff --git a/swarmserver.js b/swarmserver.js index e1071a5a..beb268ae 100644 --- a/swarmserver.js +++ b/swarmserver.js @@ -23,10 +23,10 @@ module.exports.CreateSwarmServer = function (parent, db, args, certificates) { obj.legacyAgentConnections = {}; obj.migrationAgents = {}; obj.agentActionCount = {}; - const common = require('./common.js'); - //const net = require('net'); + obj.stats = { blockedConnect: 0, connectCount: 0, clientCertConnectCount: 0, noCertConnectCount: 0, bytesIn: 0, bytesOut: 0, httpGetRequest: 0, pushedAgents: {}, close: 0, onclose: 0 } const tls = require('tls'); const forge = require('node-forge'); + const common = require('./common.js'); const LegacyMeshProtocol = { NODEPUSH: 1, // Used to send a node block to another peer. @@ -149,26 +149,41 @@ module.exports.CreateSwarmServer = function (parent, db, args, certificates) { // Called when a legacy agent connects to this server function onConnection(socket) { // Check for blocked IP address - if (checkSwarmIpAddress(socket, obj.args.swarmallowedip) == false) { Debug(1, "SWARM:New blocked agent connection"); return; } + if (checkSwarmIpAddress(socket, obj.args.swarmallowedip) == false) { obj.stats.blockedConnect++; Debug(1, "SWARM:New blocked agent connection"); return; } + obj.stats.connectCount++; socket.tag = { first: true, clientCert: socket.getPeerCertificate(true), accumulator: "", socket: socket }; socket.setEncoding('binary'); socket.pingTimer = setInterval(function () { obj.SendCommand(socket, LegacyMeshProtocol.PING); }, 20000); Debug(1, 'SWARM:New legacy agent connection'); + if ((socket.tag.clientCert == null) || (socket.tag.clientCert.subject == null)) { obj.stats.noCertConnectCount++; } else { obj.stats.clientCertConnectCount++; } + socket.addListener("data", function (data) { if (args.swarmdebug) { var buf = Buffer.from(data, "binary"); console.log('SWARM <-- (' + buf.length + '):' + buf.toString('hex')); } // Print out received bytes + obj.stats.bytesIn += data.length; socket.tag.accumulator += data; // Detect if this is an HTTPS request, if it is, return a simple answer and disconnect. This is useful for debugging access to the MPS port. if (socket.tag.first == true) { if (socket.tag.accumulator.length < 3) return; - if (socket.tag.accumulator.substring(0, 3) == 'GET') { /*console.log("Swarm Connection, HTTP GET detected: " + socket.remoteAddress);*/ socket.write('HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\nMeshCentral2 legacy swarm server.
MeshCentral1 mesh agents should connect here for updates.'); socket.end(); return; } + if (socket.tag.accumulator.substring(0, 3) == 'GET') { + obj.stats.httpGetRequest++; + /*console.log("Swarm Connection, HTTP GET detected: " + socket.remoteAddress);*/ + socket.write('HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\nMeshCentral2 legacy swarm server.
MeshCentral1 mesh agents should connect here for updates.'); + socket.end(); + return; + } socket.tag.first = false; } // A client certificate is required - if (!socket.tag.clientCert.subject) { /*console.log("Swarm Connection, no client cert: " + socket.remoteAddress);*/ socket.write('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nMeshCentral2 legacy swarm server.\r\nNo client certificate given.'); socket.end(); return; } + if ((socket.tag.clientCert == null) || (socket.tag.clientCert.subject == null)) { + /*console.log("Swarm Connection, no client cert: " + socket.remoteAddress);*/ + socket.write('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nMeshCentral2 legacy swarm server.\r\nNo client certificate given.'); + socket.end(); + return; + } try { // Parse all of the agent binary command data we can @@ -215,6 +230,10 @@ module.exports.CreateSwarmServer = function (parent, db, args, certificates) { socket.tag.updatePtr = 0; //console.log('Performing legacy agent update from ' + nodeblock.agentversion + '.' + nodeblock.agenttype + ' to ' + socket.tag.update.ver + '.' + socket.tag.update.arch + ' on ' + nodeblock.agentname + '.'); + // Update stats + if (obj.stats.pushedAgents[nodeblock.agenttype] == null) { obj.stats.pushedAgents[nodeblock.agenttype] = {}; } + if (obj.stats.pushedAgents[nodeblock.agenttype][nextAgentVersion] == null) { obj.stats.pushedAgents[nodeblock.agenttype][nextAgentVersion] = 0; } else { obj.stats.pushedAgents[nodeblock.agenttype][nextAgentVersion]++; } + // Start the agent download using the task limiter so not to flood the server. Low priority task obj.parent.taskLimiter.launch(function (socket, taskid, taskLimiterQueue) { if (socket.xclosed == 1) { @@ -286,6 +305,7 @@ module.exports.CreateSwarmServer = function (parent, db, args, certificates) { } socket.addListener("close", function () { + obj.stats.onclose++; Debug(1, 'Swarm:Connection closed'); if (socket.pingTimer != null) { clearInterval(socket.pingTimer); delete socket.pingTimer; } if (socket.tag && (typeof socket.tag.taskid == 'number')) { @@ -358,6 +378,7 @@ module.exports.CreateSwarmServer = function (parent, db, args, certificates) { // Disconnect legacy agent connection obj.close = function (socket) { + obj.stats.close++; try { socket.close(); } catch (e) { } socket.xclosed = 1; }; @@ -368,6 +389,7 @@ module.exports.CreateSwarmServer = function (parent, db, args, certificates) { }; function Write(socket, data) { + obj.stats.bytesOut += data.length; if (args.swarmdebug) { // Print out sent bytes var buf = Buffer.from(data, "binary"); diff --git a/views/default-min.handlebars b/views/default-min.handlebars index 0bd27d60..3987bfa3 100644 --- a/views/default-min.handlebars +++ b/views/default-min.handlebars @@ -1 +1 @@ - MeshCentral
{{{title}}}
{{{title2}}}

{{{logoutControl}}}

 

\ No newline at end of file + MeshCentral
{{{title}}}
{{{title2}}}

{{{logoutControl}}}

 

\ No newline at end of file diff --git a/views/default-mobile-min.handlebars b/views/default-mobile-min.handlebars index 1e36546c..1ffcfed7 100644 --- a/views/default-mobile-min.handlebars +++ b/views/default-mobile-min.handlebars @@ -1 +1 @@ - MeshCentral
{{{title}}}
{{{title2}}}
\ No newline at end of file + MeshCentral
{{{title}}}
{{{title2}}}
\ No newline at end of file diff --git a/views/default-mobile.handlebars b/views/default-mobile.handlebars index 077d9ca0..d05dffe9 100644 --- a/views/default-mobile.handlebars +++ b/views/default-mobile.handlebars @@ -243,7 +243,7 @@
Change email address
-
Change password
+
Change password
Delete account

@@ -654,8 +654,21 @@ QV('verifyEmailId', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true)); QV('manageAuthApp', features & 4096); QV('manageOtp', ((features & 4096) != 0) && ((userinfo.otpsecret == 1) || (userinfo.otphkeys > 0))); + + if (typeof userinfo.passchange == 'number') { + if (userinfo.passchange == -1) { QH('p2nextPasswordUpdateTime', ' - Reset on next login.'); } + else if ((passRequirements != null) && (typeof passRequirements.reset == 'number')) { + var seconds = (userinfo.passchange) + (passRequirements.reset * 86400) - Math.floor(Date.now() / 1000); + if (seconds < 0) { QH('p2nextPasswordUpdateTime', ' - Reset on next login.'); } + else if (seconds < 3600) { QH('p2nextPasswordUpdateTime', ' - Reset in ' + Math.floor(seconds / 60) + ' minute' + addLetterS(Math.floor(seconds / 60)) + '.'); } + else if (seconds < 86400) { QH('p2nextPasswordUpdateTime', ' - Reset in ' + Math.floor(seconds / 3600) + ' hour' + addLetterS(Math.floor(seconds / 3600)) + '.'); } + else { QH('p2nextPasswordUpdateTime', ' - Reset in ' + Math.floor(seconds / 86400) + ' day' + addLetterS(Math.floor(seconds / 86400)) + '.'); } + } + } } + function addLetterS(x) { return (x > 1) ? 's' : ''; } + function onMessage(server, message) { switch (message.action) { case 'serverinfo': { diff --git a/views/default.handlebars b/views/default.handlebars index cb788a8c..03078350 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -260,7 +260,7 @@

Change email address
- Change password
+ Change password
Delete account


@@ -1190,8 +1190,21 @@ QV('authAppSetupCheck', userinfo.otpsecret == 1); QV('authKeySetupCheck', userinfo.otphkeys > 0); QV('authCodesSetupCheck', userinfo.otpkeys > 0); + + if (typeof userinfo.passchange == 'number') { + if (userinfo.passchange == -1) { QH('p2nextPasswordUpdateTime', ' - Reset on next login.'); } + else if ((passRequirements != null) && (typeof passRequirements.reset == 'number')) { + var seconds = (userinfo.passchange) + (passRequirements.reset * 86400) - Math.floor(Date.now() / 1000); + if (seconds < 0) { QH('p2nextPasswordUpdateTime', ' - Reset on next login.'); } + else if (seconds < 3600) { QH('p2nextPasswordUpdateTime', ' - Reset in ' + Math.floor(seconds / 60) + ' minute' + addLetterS(Math.floor(seconds / 60)) + '.'); } + else if (seconds < 86400) { QH('p2nextPasswordUpdateTime', ' - Reset in ' + Math.floor(seconds / 3600) + ' hour' + addLetterS(Math.floor(seconds / 3600)) + '.'); } + else { QH('p2nextPasswordUpdateTime', ' - Reset in ' + Math.floor(seconds / 86400) + ' day' + addLetterS(Math.floor(seconds / 86400)) + '.'); } + } + } } + function addLetterS(x) { return (x > 1) ? 's' : ''; } + function onMessage(server, message) { switch (message.action) { case 'serverstats': { @@ -1767,7 +1780,9 @@ } case 'login': { // Update the last login time - if (users != null && users['user/' + domain + '/' + message.event.username.toLowerCase()]) { users['user/' + domain + '/' + message.event.username.toLowerCase()].login = Math.floor(message.event.time / 1000); } + if (users != null && users['user/' + domain + '/' + message.event.username.toLowerCase()]) { + users['user/' + domain + '/' + message.event.username.toLowerCase()].login = Math.floor(new Date(message.event.time).getTime() / 1000); + } break; } case 'scanamtdevice': { @@ -6584,6 +6599,8 @@ if (user.quota) x += addDeviceAttribute('Server Quota', EscapeHtml(parseInt(user.quota) / 1024) + ' k'); x += addDeviceAttribute('Creation', new Date(user.creation * 1000).toLocaleString()); if (user.login) x += addDeviceAttribute('Last Login', new Date(user.login * 1000).toLocaleString()); + if (user.passchange == -1) { x += addDeviceAttribute('Password', 'Will be changed on next login.'); } + else if (user.passchange) { x += addDeviceAttribute('Password', 'Last changed: ' + new Date(user.passchange * 1000).toLocaleString()); } var linkCount = 0, linkCountStr = 'None'; if (user.links) { @@ -6674,6 +6691,7 @@ x += addHtmlValue('Password', ''); x += addHtmlValue('Password hint', ''); if (passRequirements) { var r = []; for (var i in passRequirements) { r.push(i + ':' + passRequirements[i]); } x += '
Requirements: ' + r.join(', ') + '.
'; } + x += '
Force password reset on next login.
'; if (multiFactor == 1) { x += '
Remove all 2nd factor authentication.
'; } setDialogMode(2, "Change Password for " + EscapeHtml(currentUser.name), 3, p30showUserChangePassDialogEx, x, multiFactor); showCreateNewAccountDialogValidate(1); @@ -6683,7 +6701,7 @@ function p30showUserChangePassDialogEx(b, tag) { var removeMultiFactor = false; if ((tag == 1) && (Q('p4twoFactorRemove').checked == true)) { removeMultiFactor = true; } - if (Q('p4pass1').value == Q('p4pass2').value) { meshserver.send({ action: 'changeuserpass', user: currentUser.name, pass: Q('p4pass1').value, hint: Q('p4hint').value, removeMultiFactor: removeMultiFactor }); } + if (Q('p4pass1').value == Q('p4pass2').value) { meshserver.send({ action: 'changeuserpass', user: currentUser.name, pass: Q('p4pass1').value, hint: Q('p4hint').value, removeMultiFactor: removeMultiFactor, resetNextLogin: Q('p4resetNextLogin').checked }); } } function p30showDeleteUserDialog() { diff --git a/views/login-min.handlebars b/views/login-min.handlebars index bdfe4091..7de9e556 100644 --- a/views/login-min.handlebars +++ b/views/login-min.handlebars @@ -1 +1 @@ - MeshCentral - Login
{{{title}}}
{{{title2}}}

Welcome


\ No newline at end of file + MeshCentral - Login
{{{title}}}
{{{title2}}}

Welcome


\ No newline at end of file diff --git a/views/login-mobile-min.handlebars b/views/login-mobile-min.handlebars index f0c8f4ac..7bd32300 100644 --- a/views/login-mobile-min.handlebars +++ b/views/login-mobile-min.handlebars @@ -1 +1 @@ - MeshCentral - Login
{{{title}}}
{{{title2}}}
\ No newline at end of file + MeshCentral - Login
{{{title}}}
{{{title2}}}
\ No newline at end of file diff --git a/views/login-mobile.handlebars b/views/login-mobile.handlebars index 90965802..f678b25e 100644 --- a/views/login-mobile.handlebars +++ b/views/login-mobile.handlebars @@ -188,6 +188,35 @@ + @@ -288,6 +317,7 @@ QV('message3', false); QV('message4', false); QV('message5', false); + QV('message6', false); go(x); } @@ -299,10 +329,13 @@ QV('resetpanel', x == 3); QV('tokenpanel', x == 4); QV('resettokenpanel', x == 5); + QV('resetpasswordpanel', x == 6); if (x == 1) { Q('username').focus(); } if (x == 2) { Q('ausername').focus(); } if (x == 3) { Q('remail').focus(); } if (x == 4) { Q('tokenInput').focus(); } + if (x == 5) { Q('resetTokenInput').focus(); } + if (x == 6) { Q('rapassword1').focus(); } } function validateLogin(box, e) { @@ -355,6 +388,52 @@ if (e != null) { haltEvent(e); } } + function validatePassReset(box, e) { + setDialogMode(0); + var pass1ok = (Q('rapassword1').value.length > 0); + var pass2ok = (Q('rapassword2').value.length > 0) && (Q('rapassword2').value == Q('rapassword1').value); + var ok = (pass1ok && pass2ok); + + // Color the fields + QS('rnuPass1').color = pass1ok ? 'black' : '#7b241c'; + QS('rnuPass2').color = pass2ok ? 'black' : '#7b241c'; + + if (Q('rapassword1').value == '') { + QH('rpassWarning', ''); + QV('rpasswordPolicyCallout', false); + } else { + if (passRequirements == null || passRequirements == '') { + // No password requirements, display password strength + var passStrength = checkPasswordStrength(Q('rapassword1').value); + if (passStrength >= 80) { QH('rpassWarning', 'Strong Password'); } + else if (passStrength >= 60) { QH('rpassWarning', 'Good Password'); } + else { QH('rpassWarning', 'Weak Password'); } + } else { + // Password requirements provided, use that + var passReq = checkPasswordRequirements(Q('rapassword1').value, passRequirements); + if (passReq == false) { + ok = false; + QS('rnuPass1').color = '#7b241c'; + QS('rnuPass2').color = '#7b241c'; + QH('rpassWarning', '
Password Policy
'); // This is also a link to the password policy + QV('rpasswordPolicyCallout', true); + QH('rpasswordPolicyCallout', passwordPolicyText(Q('rapassword1').value)); + } else { + QH('rpassWarning', ''); + QV('rpasswordPolicyCallout', false); + } + } + } + if ((e != null) && (e.keyCode == 13)) { + if (box == 2) { Q('rapassword1').focus(); } + if (box == 3) { Q('rapassword2').focus(); } + if (box == 4) { Q('rapasswordhint').focus(); } + if (box == 6) { Q('resetPassButton').click(); } + } + if (e != null) { haltEvent(e); } + QE('resetPassButton', ok); + } + function validateReset(e) { setDialogMode(0); var x = validateEmail(Q('remail').value); diff --git a/views/login.handlebars b/views/login.handlebars index 72a13756..866eedcf 100644 --- a/views/login.handlebars +++ b/views/login.handlebars @@ -167,19 +167,19 @@ Password: - + Password: - + Password Hint: - + Creation Token: - + @@ -258,6 +258,35 @@
Back to login
+ @@ -373,6 +402,7 @@ QV('message3', false); QV('message4', false); QV('message5', false); + QV('message6', false); go(x); } @@ -384,10 +414,13 @@ QV('resetpanel', x == 3); QV('tokenpanel', x == 4); QV('resettokenpanel', x == 5); + QV('resetpasswordpanel', x == 6); if (x == 1) { Q('username').focus(); } if (x == 2) { Q('ausername').focus(); } if (x == 3) { Q('remail').focus(); } if (x == 4) { Q('tokenInput').focus(); } + if (x == 5) { Q('resetTokenInput').focus(); } + if (x == 6) { Q('rapassword1').focus(); } } function validateLogin(box, e) { @@ -452,6 +485,52 @@ QE('createButton', ok); } + function validatePassReset(box, e) { + setDialogMode(0); + var pass1ok = (Q('rapassword1').value.length > 0); + var pass2ok = (Q('rapassword2').value.length > 0) && (Q('rapassword2').value == Q('rapassword1').value); + var ok = (pass1ok && pass2ok); + + // Color the fields + QS('rnuPass1').color = pass1ok ? 'black' : '#7b241c'; + QS('rnuPass2').color = pass2ok ? 'black' : '#7b241c'; + + if (Q('rapassword1').value == '') { + QH('rpassWarning', ''); + QV('rpasswordPolicyCallout', false); + } else { + if (passRequirements == null || passRequirements == '') { + // No password requirements, display password strength + var passStrength = checkPasswordStrength(Q('rapassword1').value); + if (passStrength >= 80) { QH('rpassWarning', 'Strong Password'); } + else if (passStrength >= 60) { QH('rpassWarning', 'Good Password'); } + else { QH('rpassWarning', 'Weak Password'); } + } else { + // Password requirements provided, use that + var passReq = checkPasswordRequirements(Q('rapassword1').value, passRequirements); + if (passReq == false) { + ok = false; + QS('rnuPass1').color = '#7b241c'; + QS('rnuPass2').color = '#7b241c'; + QH('rpassWarning', '
Password Policy
'); // This is also a link to the password policy + QV('rpasswordPolicyCallout', true); + QH('rpasswordPolicyCallout', passwordPolicyText(Q('rapassword1').value)); + } else { + QH('rpassWarning', ''); + QV('rpasswordPolicyCallout', false); + } + } + } + if ((e != null) && (e.keyCode == 13)) { + if (box == 2) { Q('rapassword1').focus(); } + if (box == 3) { Q('rapassword2').focus(); } + if (box == 4) { Q('rapasswordhint').focus(); } + if (box == 6) { Q('resetPassButton').click(); } + } + if (e != null) { haltEvent(e); } + QE('resetPassButton', ok); + } + function passwordPolicyText(pass) { var policy = '
'; var counts = strCount(pass); diff --git a/webserver.js b/webserver.js index 738c7607..b398ab2b 100644 --- a/webserver.js +++ b/webserver.js @@ -419,7 +419,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { var user = obj.users[userid]; // Check if this user has 2-step login active - if (checkUserOneTimePasswordRequired(domain, user)) { + if ((req.session.loginmode != '6') && checkUserOneTimePasswordRequired(domain, user)) { checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result) { if (result == false) { var randomWaitTime = 0; @@ -439,14 +439,14 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { }, randomWaitTime); } else { // Login succesful - completeLoginRequest(req, res, domain, user, userid); + completeLoginRequest(req, res, domain, user, userid, xusername, xpassword); } }); return; } // Login succesful - completeLoginRequest(req, res, domain, user, userid); + completeLoginRequest(req, res, domain, user, userid, xusername, xpassword); } else { // Login failed, wait a random delay setTimeout(function () { @@ -466,10 +466,22 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { }); } - function completeLoginRequest(req, res, domain, user, userid) { + function completeLoginRequest(req, res, domain, user, userid, xusername, xpassword) { + // Check if we need to change the password + if ((typeof user.passchange == 'number') && ((user.passchange == -1) || ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.reset == 'number') && (user.passchange + (domain.passwordrequirements.reset * 86400) < Math.floor(Date.now() / 1000))))) { + // Request a password change + req.session.loginmode = '6'; + req.session.error = 'Password change requested.'; + req.session.resettokenusername = xusername; + req.session.resettokenpassword = xpassword; + res.redirect(domain.url); + return; + } + // Save login time user.login = Math.floor(Date.now() / 1000); obj.db.SetUser(user); + obj.parent.DispatchEvent(['*'], obj, { etype: 'user', username: user.name, account: obj.CloneSafeUser(user), action: 'login', msg: 'Account login', domain: domain.id }); // Regenerate session when signing in to prevent fixation //req.session.regenerate(function () { @@ -506,8 +518,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { res.redirect(domain.url); } //}); - - obj.parent.DispatchEvent(['*'], obj, { etype: 'user', username: user.name, action: 'login', msg: 'Account login', domain: domain.id }); } function handleCreateAccountRequest(req, res) { @@ -521,7 +531,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { var i = -1; if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); } if (i == -1) { - req.session.loginmode = 2; + req.session.loginmode = '2'; req.session.error = 'Unable to create account.'; res.redirect(domain.url); return; @@ -529,7 +539,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase(); for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } } if (emailok == false) { - req.session.loginmode = 2; + req.session.loginmode = '2'; req.session.error = 'Unable to create account.'; res.redirect(domain.url); return; @@ -539,33 +549,33 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Check if we exceed the maximum number of user accounts obj.db.isMaxType(domain.limits.maxuseraccounts, 'user', domain.id, function (maxExceed) { if (maxExceed) { - req.session.loginmode = 2; + req.session.loginmode = '2'; req.session.error = 'Account limit reached.'; console.log('max', req.session); res.redirect(domain.url); } else { if (!obj.common.validateUsername(req.body.username, 1, 64) || !obj.common.validateEmail(req.body.email, 1, 256) || !obj.common.validateString(req.body.password1, 1, 256) || !obj.common.validateString(req.body.password2, 1, 256) || (req.body.password1 != req.body.password2) || req.body.username == '~' || !obj.common.checkPasswordRequirements(req.body.password1, domain.passwordrequirements)) { - req.session.loginmode = 2; + req.session.loginmode = '2'; req.session.error = 'Unable to create account.'; res.redirect(domain.url); } else { // Check if this email was already verified obj.db.GetUserWithVerifiedEmail(domain.id, req.body.email, function (err, docs) { if (docs.length > 0) { - req.session.loginmode = 2; + req.session.loginmode = '2'; req.session.error = 'Existing account with this email address.'; res.redirect(domain.url); } else { // Check if there is domain.newAccountToken, check if supplied token is valid if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.anewaccountpass != domain.newaccountspass)) { - req.session.loginmode = 2; + req.session.loginmode = '2'; req.session.error = 'Invalid account creation token.'; res.redirect(domain.url); return; } // Check if user exists if (obj.users['user/' + domain.id + '/' + req.body.username.toLowerCase()]) { - req.session.loginmode = 2; + req.session.loginmode = '2'; req.session.error = 'Username already exists.'; } else { var hint = req.body.apasswordhint; @@ -599,6 +609,82 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { }); } + // Called to process an account password reset + function handleResetPasswordRequest(req, res) { + const domain = checkUserIpAddress(req, res); + + // Check everything is ok + if ((domain == null) || (domain.auth == 'sspi') || (typeof req.body.rpassword1 != 'string') || (typeof req.body.rpassword2 != 'string') || (req.body.rpassword1 != req.body.rpassword2) || (typeof req.body.rpasswordhint != 'string') || (req.session == null) || (typeof req.session.resettokenusername != 'string') || (typeof req.session.resettokenpassword != 'string')) { + delete req.session.loginmode; + delete req.session.tokenusername; + delete req.session.tokenpassword; + delete req.session.resettokenusername; + delete req.session.resettokenpassword; + delete req.session.tokenemail; + delete req.session.success; + delete req.session.error; + delete req.session.passhint; + res.redirect(domain.url); + return; + } + + // Authenticate the user + obj.authenticate(req.session.resettokenusername, req.session.resettokenpassword, domain, function (err, userid, passhint) { + if (userid) { + // Login + var user = obj.users[userid]; + + // If we have password requirements, check this here. + if (!obj.common.checkPasswordRequirements(req.body.rpassword1, domain.passwordrequirements)) { + req.session.loginmode = '6'; + req.session.error = 'Password rejected, use a different one.'; + res.redirect(domain.url); + return; + } + + // Check if the password is the same as the previous one + require('./pass').hash(req.body.rpassword1, user.salt, function (err, hash) { + if (user.hash == hash) { + // This is the same password, request a password change again + req.session.loginmode = '6'; + req.session.error = 'Password rejected, use a different one.'; + res.redirect(domain.url); + } else { + // Update the password, use a different salt. + require('./pass').hash(req.body.rpassword1, function (err, salt, hash) { + if (err) throw err; + user.salt = salt; + user.hash = hash; + user.passhint = req.body.rpasswordhint; + user.passchange = Math.floor(Date.now() / 1000); + delete user.passtype; + obj.db.SetUser(user); + obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, { etype: 'user', username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'User password reset', domain: domain.id }); + + // Login succesful + req.session.userid = userid; + req.session.domainid = domain.id; + completeLoginRequest(req, res, domain, obj.users[userid], userid, req.session.tokenusername, req.session.tokenpassword); + }); + } + }); + } else { + // Failed, error out. + delete req.session.loginmode; + delete req.session.tokenusername; + delete req.session.tokenpassword; + delete req.session.resettokenusername; + delete req.session.resettokenpassword; + delete req.session.tokenemail; + delete req.session.success; + delete req.session.error; + delete req.session.passhint; + res.redirect(domain.url); + return; + } + }); + } + // Called to process an account reset request function handleResetAccountRequest(req, res) { const domain = checkUserIpAddress(req, res); @@ -610,13 +696,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Check the email stirng format if (!email || checkEmail(email) == false) { - req.session.loginmode = 3; + req.session.loginmode = '3'; req.session.error = 'Invalid email.'; res.redirect(domain.url); } else { obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) { if ((err != null) || (docs.length == 0)) { - req.session.loginmode = 3; + req.session.loginmode = '3'; req.session.error = 'Account not found.'; res.redirect(domain.url); } else { @@ -635,11 +721,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { delete req.session.tokenemail; if (obj.parent.mailserver != null) { obj.parent.mailserver.sendAccountResetMail(domain, user.name, user.email); - req.session.loginmode = 1; + req.session.loginmode = '1'; req.session.error = 'Hold on, reset mail sent.'; res.redirect(domain.url); } else { - req.session.loginmode = 3; + req.session.loginmode = '3'; req.session.error = 'Unable to sent email.'; res.redirect(domain.url); } @@ -649,11 +735,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // No second factor, send email to perform recovery. if (obj.parent.mailserver != null) { obj.parent.mailserver.sendAccountResetMail(domain, user.name, user.email); - req.session.loginmode = 1; + req.session.loginmode = '1'; req.session.error = 'Hold on, reset mail sent.'; res.redirect(domain.url); } else { - req.session.loginmode = 3; + req.session.loginmode = '3'; req.session.error = 'Unable to sent email.'; res.redirect(domain.url); } @@ -1074,7 +1160,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if (err != null) message = '

' + err + '

'; if (msg != null) message = '

' + msg + '

'; if (passhint != null) passhint = EscapeHtml(passhint); - var emailcheck = ((obj.parent.mailserver != null) && (domain.auth != 'sspi')); if (obj.args.minify && !req.query.nominify) { @@ -2271,6 +2356,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { obj.app.post(url + 'changepassword', handlePasswordChangeRequest); obj.app.post(url + 'deleteaccount', handleDeleteAccountRequest); obj.app.post(url + 'createaccount', handleCreateAccountRequest); + obj.app.post(url + 'resetpassword', handleResetPasswordRequest); obj.app.post(url + 'resetaccount', handleResetAccountRequest); obj.app.get(url + 'checkmail', handleCheckMailRequest); obj.app.post(url + 'amtevents.ashx', obj.handleAmtEventRequest);