Added user account, user session and agent session per-domain limits.

This commit is contained in:
Ylian Saint-Hilaire 2019-02-11 14:41:15 -08:00
parent a932a66044
commit a0edd68ac4
11 changed files with 212 additions and 29 deletions

View File

@ -112,7 +112,9 @@ module.exports.CertificateOperations = function (parent) {
// Create a self-signed certificate // Create a self-signed certificate
obj.GenerateRootCertificate = function (addThumbPrintToName, commonName, country, organization, strong) { obj.GenerateRootCertificate = function (addThumbPrintToName, commonName, country, organization, strong) {
var keys = obj.pki.rsa.generateKeyPair((strong == true) ? 3072 : 2048); // TODO: Use Async key generation to use web workers and go a lot faster.
// rsa.generateKeyPair({ bits: 3072, e: 0x10001, workers: -1 }, function (err, keypair) { /*keypair.privateKey, keypair.publicKey*/ });
var keys = obj.pki.rsa.generateKeyPair({ bits: (strong == true) ? 3072 : 2048, e: 0x10001 });
var cert = obj.pki.createCertificate(); var cert = obj.pki.createCertificate();
cert.publicKey = keys.publicKey; cert.publicKey = keys.publicKey;
cert.serialNumber = String(Math.floor((Math.random() * 100000) + 1)); cert.serialNumber = String(Math.floor((Math.random() * 100000) + 1));
@ -136,7 +138,7 @@ module.exports.CertificateOperations = function (parent) {
// Issue a certificate from a root // Issue a certificate from a root
obj.IssueWebServerCertificate = function (rootcert, addThumbPrintToName, commonName, country, organization, extKeyUsage, strong) { obj.IssueWebServerCertificate = function (rootcert, addThumbPrintToName, commonName, country, organization, extKeyUsage, strong) {
var keys = obj.pki.rsa.generateKeyPair((strong == true) ? 3072 : 2048); var keys = obj.pki.rsa.generateKeyPair({ bits: (strong == true) ? 3072 : 2048, e: 0x10001 });
var cert = obj.pki.createCertificate(); var cert = obj.pki.createCertificate();
cert.publicKey = keys.publicKey; cert.publicKey = keys.publicKey;
cert.serialNumber = String(Math.floor((Math.random() * 100000) + 1)); cert.serialNumber = String(Math.floor((Math.random() * 100000) + 1));

2
db.js
View File

@ -175,7 +175,7 @@ module.exports.CreateDB = function (parent) {
obj.getPowerTimeline = function (nodeid, func) { if (obj.databaseType == 1) { obj.file.find({ type: 'power', node: { $in: ['*', nodeid] } }).sort({ time: 1 }).exec(func); } else { obj.file.find({ type: 'power', node: { $in: ['*', nodeid] } }).sort({ time: 1 }, func); } }; obj.getPowerTimeline = function (nodeid, func) { if (obj.databaseType == 1) { obj.file.find({ type: 'power', node: { $in: ['*', nodeid] } }).sort({ time: 1 }).exec(func); } else { obj.file.find({ type: 'power', node: { $in: ['*', nodeid] } }).sort({ time: 1 }, func); } };
obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }, func); }; obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }, func); };
obj.getAmtUuidNode = function (meshid, uuid, func) { obj.file.find({ type: 'node', meshid: meshid, 'intelamt.uuid': uuid }, func); }; obj.getAmtUuidNode = function (meshid, uuid, func) { obj.file.find({ type: 'node', meshid: meshid, 'intelamt.uuid': uuid }, func); };
obj.isMaxType = function (max, type, func) { if (max == null) { func(false); } else { obj.file.count({ type: type }, function (err, count) { func((err != null) || (count > max)); }); } } obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max)); }); } }
// Read a configuration file from the database // Read a configuration file from the database
obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); } obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }

View File

@ -368,6 +368,16 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
if ((obj.authenticated != 1) || (obj.meshid == null) || obj.pendingCompleteAgentConnection) return; if ((obj.authenticated != 1) || (obj.meshid == null) || obj.pendingCompleteAgentConnection) return;
obj.pendingCompleteAgentConnection = true; obj.pendingCompleteAgentConnection = true;
// Check if we have too many agent sessions
if (typeof domain.limits.maxagentsessions == 'number') {
// Count the number of agent sessions for this domain
var domainAgentSessionCount = 0;
for (var i in obj.parent.wsagents) { if (obj.parent.wsagents[i].domain.id == domain.id) { domainAgentSessionCount++; } }
// Check if we have too many user sessions
if (domainAgentSessionCount >= domain.limits.maxagentsessions) { return; } // Too many, hold the connection.
}
// Check that the mesh exists // Check that the mesh exists
var mesh = obj.parent.meshes[obj.dbMeshKey]; var mesh = obj.parent.meshes[obj.dbMeshKey];
if (mesh == null) { console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').'); return; } // If we disconnect, the agnet will just reconnect. We need to log this or tell agent to connect in a few hours. if (mesh == null) { console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').'); return; } // If we disconnect, the agnet will just reconnect. We need to log this or tell agent to connect in a few hours.

View File

@ -437,6 +437,7 @@ function CreateMeshCentralServer(config, args) {
var bannedDomains = ['public', 'private', 'images', 'scripts', 'styles', 'views']; // List of banned domains var bannedDomains = ['public', 'private', 'images', 'scripts', 'styles', 'views']; // List of banned domains
for (i in obj.config.domains) { for (var j in bannedDomains) { if (i == bannedDomains[j]) { console.log("ERROR: Domain '" + i + "' is not allowed domain name in ./data/config.json."); return; } } } for (i in obj.config.domains) { for (var j in bannedDomains) { if (i == bannedDomains[j]) { console.log("ERROR: Domain '" + i + "' is not allowed domain name in ./data/config.json."); return; } } }
for (i in obj.config.domains) { for (i in obj.config.domains) {
if (obj.config.domains[i].limits == null) { obj.config.domains[i].limits = {}; }
if (obj.config.domains[i].dns == null) { obj.config.domains[i].url = (i == '') ? '/' : ('/' + i + '/'); } else { obj.config.domains[i].url = '/'; } if (obj.config.domains[i].dns == null) { obj.config.domains[i].url = (i == '') ? '/' : ('/' + i + '/'); } else { obj.config.domains[i].url = '/'; }
obj.config.domains[i].id = i; obj.config.domains[i].id = i;
if (typeof obj.config.domains[i].userallowedip == 'string') { if (obj.config.domains[i].userallowedip == '') { obj.config.domains[i].userallowedip = null; } else { obj.config.domains[i].userallowedip = obj.config.domains[i].userallowedip.split(','); } } if (typeof obj.config.domains[i].userallowedip == 'string') { if (obj.config.domains[i].userallowedip == '') { obj.config.domains[i].userallowedip = null; } else { obj.config.domains[i].userallowedip = obj.config.domains[i].userallowedip.split(','); } }

View File

@ -127,6 +127,20 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
// Check if the user is logged in // Check if the user is logged in
if (user == null) { try { obj.ws.close(); } catch (e) { } return; } if (user == null) { try { obj.ws.close(); } catch (e) { } return; }
// Check if we have exceeded the user session limit
if (typeof domain.limits.maxusersessions == 'number') {
// Count the number of user sessions for this domain
var domainUserSessionCount = 0;
for (var i in obj.parent.wssessions2) { if (obj.parent.wssessions2[i].domainid == domain.id) { domainUserSessionCount++; } }
// Check if we have too many user sessions
if (domainUserSessionCount >= domain.limits.maxusersessions) {
ws.send(JSON.stringify({ action: 'stopped', msg: 'Session count exceed' }));
try { obj.ws.close(); } catch (e) { }
return;
}
}
// Associate this websocket session with the web session // Associate this websocket session with the web session
obj.ws.userid = req.session.userid; obj.ws.userid = req.session.userid;
obj.ws.domainid = domain.id; obj.ws.domainid = domain.id;
@ -643,7 +657,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
if (obj.parent.users[newuserid]) break; // Account already exists if (obj.parent.users[newuserid]) break; // Account already exists
// Check if we exceed the maximum number of user accounts // Check if we exceed the maximum number of user accounts
obj.db.isMaxType(domain.maxaccounts, 'user', function (maxExceed) { obj.db.isMaxType(domain.limits.maxuseraccounts, 'user', domain.id, function (maxExceed) {
if (maxExceed) { if (maxExceed) {
// Account count exceed, do notification // Account count exceed, do notification

View File

@ -1,6 +1,6 @@
{ {
"name": "meshcentral", "name": "meshcentral",
"version": "0.2.7-u", "version": "0.2.7-v",
"keywords": [ "keywords": [
"Remote Management", "Remote Management",
"Intel AMT", "Intel AMT",

View File

@ -41,6 +41,11 @@
"_UserBlockedIP": "127.0.0.1,::1,192.168.0.100", "_UserBlockedIP": "127.0.0.1,::1,192.168.0.100",
"_AgentAllowedIP": "192.168.0.100/24", "_AgentAllowedIP": "192.168.0.100/24",
"_AgentBlockedIP": "127.0.0.1,::1", "_AgentBlockedIP": "127.0.0.1,::1",
"_Limits": {
"MaxUserAccounts": 100,
"MaxUserSessions": 100,
"MaxAgentSessions": 100
},
"_yubikey": { "id": "0000", "secret": "xxxxxxxxxxxxxxxxxxxxx", "_proxy": "http://myproxy.domain.com:80" }, "_yubikey": { "id": "0000", "secret": "xxxxxxxxxxxxxxxxxxxxx", "_proxy": "http://myproxy.domain.com:80" },
}, },
"customer1": { "customer1": {

View File

@ -855,6 +855,7 @@
<script type="text/javascript"> <script type="text/javascript">
'use strict'; 'use strict';
var args; var args;
var autoReconnect = true;
var powerStatetable = ['', 'Powered', 'Sleep', 'Sleep', 'Sleep', 'Hibernating', 'Power off', 'Present']; var powerStatetable = ['', 'Powered', 'Sleep', 'Sleep', 'Sleep', 'Hibernating', 'Power off', 'Present'];
var StatusStrs = ['Disconnected', 'Connecting...', 'Setup...', 'Connected', 'Intel&reg; AMT Connected']; var StatusStrs = ['Disconnected', 'Connecting...', 'Setup...', 'Connected', 'Intel&reg; AMT Connected'];
var sort = 0; var sort = 0;
@ -1076,7 +1077,7 @@
QV('verifyEmailId2', false); QV('verifyEmailId2', false);
QV('logoutControl', false); QV('logoutControl', false);
if (errorCode == 'noauth') { QH('p0span', 'Unable to perform authentication'); return; } if (errorCode == 'noauth') { QH('p0span', 'Unable to perform authentication'); return; }
if (prevState == 2) { setTimeout(serverPoll, 5000); } else { QH('p0span', 'Unable to connect web socket'); } if (prevState == 2) { if (autoReconnect) { setTimeout(serverPoll, 5000); } } else { QH('p0span', 'Unable to connect web socket'); }
if (authCookieRenewTimer != null) { clearInterval(authCookieRenewTimer); authCookieRenewTimer = null; } if (authCookieRenewTimer != null) { clearInterval(authCookieRenewTimer); authCookieRenewTimer = null; }
} else if (state == 2) { } else if (state == 2) {
// Fetch list of meshes, nodes, files // Fetch list of meshes, nodes, files
@ -1751,7 +1752,8 @@
break; break;
} }
case 'stopped': { // Server is stopping. case 'stopped': { // Server is stopping.
// TODO: Disconnect // Disconnect
console.log(message.msg);
break; break;
} }
default: default:
@ -1760,6 +1762,12 @@
} }
break; break;
} }
case 'stopped': { // Server is stopping.
// Disconnect
autoReconnect = false;
QH('p0span', message.msg);
break;
}
default: default:
console.log('Unknown message.action', message.action); console.log('Unknown message.action', message.action);
break; break;
@ -6379,7 +6387,7 @@
x += addDeviceAttribute('Creation', new Date(user.creation * 1000).toLocaleString()); x += addDeviceAttribute('Creation', new Date(user.creation * 1000).toLocaleString());
if (user.login) x += addDeviceAttribute('Last Login', new Date(user.login * 1000).toLocaleString()); if (user.login) x += addDeviceAttribute('Last Login', new Date(user.login * 1000).toLocaleString());
var multiFactor = 0; var multiFactor = 0;
if ((user.otpsecret > 0) || (user.otphkeys > 0) || (user.otpkeys > 0)) { if ((user.otpsecret > 0) || (user.otphkeys > 0)) {
multiFactor = 1; multiFactor = 1;
var factors = []; var factors = [];
if (user.otpsecret > 0) { factors.push('Authentication App'); } if (user.otpsecret > 0) { factors.push('Authentication App'); }

View File

@ -150,7 +150,7 @@
<tr> <tr>
<td align=right width=100>Login token:</td> <td align=right width=100>Login token:</td>
<td> <td>
<input id=tokenInput type=text name=token maxlength=50 onkeyup=checkToken(event) onkeydown=checkToken(event) /> <input id=tokenInput type=text name=token maxlength=50 onchange=checkToken(event) onkeyup=checkToken(event) onkeydown=checkToken(event) />
<input id=hwtokenInput type=text name=hwtoken style="display:none" /> <input id=hwtokenInput type=text name=hwtoken style="display:none" />
</td> </td>
</tr> </tr>
@ -163,6 +163,31 @@
<hr /><a onclick=xgo(1) style=cursor:pointer>Back to login</a> <hr /><a onclick=xgo(1) style=cursor:pointer>Back to login</a>
</form> </form>
</div> </div>
<div id=resettokenpanel style="background-color:#979797;border-radius:16px;width:260px;padding:16px;text-align:center;display:none;clear:both">
<form action=resetaccount method=post autocomplete=off>
<div id=message5>
{{{message}}}
</div>
<table>
<tr>
<td align=right width=100>Login token:</td>
<td>
<input id=resetTokenInput type=text name=token maxlength=50 onchange=resetCheckToken(event) onkeyup=resetCheckToken(event) onkeydown=resetCheckToken(event) />
<input id=resetHwtokenInput type=text name=hwtoken style="display:none" />
</td>
</tr>
<tr>
<td colspan=2>
<div style=float:right><input id=resetTokenOkButton 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> </td>
</tr> </tr>
</table> </table>
@ -237,6 +262,19 @@
}, hardwareKeyChallenge.timeoutSeconds); }, hardwareKeyChallenge.timeoutSeconds);
} }
} }
if ('{{loginmode}}' == '5') {
try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null }
if ((hardwareKeyChallenge != null) && u2fSupported()) {
window.u2f.sign(hardwareKeyChallenge.appId, hardwareKeyChallenge.challenge, hardwareKeyChallenge.registeredKeys, function (authResponse) {
if (authResponse.signatureData) {
Q('resetHwtokenInput').value = JSON.stringify(authResponse);
QE('resetTokenOkButton', true);
Q('resetTokenOkButton').click();
}
}, hardwareKeyChallenge.timeoutSeconds);
}
}
} }
function showPassHint() { function showPassHint() {
@ -246,6 +284,9 @@
function xgo(x) { function xgo(x) {
QV('message1', false); QV('message1', false);
QV('message2', false); QV('message2', false);
QV('message3', false);
QV('message4', false);
QV('message5', false);
go(x); go(x);
} }
@ -256,6 +297,7 @@
QV('createpanel', x == 2); QV('createpanel', x == 2);
QV('resetpanel', x == 3); QV('resetpanel', x == 3);
QV('tokenpanel', x == 4); QV('tokenpanel', x == 4);
QV('resettokenpanel', x == 5);
if (x == 1) { Q('username').focus(); } if (x == 1) { Q('username').focus(); }
if (x == 2) { Q('ausername').focus(); } if (x == 2) { Q('ausername').focus(); }
if (x == 3) { Q('remail').focus(); } if (x == 3) { Q('remail').focus(); }
@ -353,6 +395,13 @@
QE('tokenOkButton', (Q('tokenInput').value.length == 6) || (Q('tokenInput').value.length == 8) || (Q('tokenInput').value.length == 44)); QE('tokenOkButton', (Q('tokenInput').value.length == 6) || (Q('tokenInput').value.length == 8) || (Q('tokenInput').value.length == 44));
} }
function resetCheckToken() {
var t1 = Q('resetTokenInput').value;
var t2 = t1.split(' ').join('');
if (t1 != t2) { Q('resetTokenInput').value = t2; }
QE('resetTokenOkButton', (Q('resetTokenInput').value.length == 6) || (Q('resetTokenInput').value.length == 8) || (Q('resetTokenInput').value.length == 44));
}
// //
// POPUP DIALOG // POPUP DIALOG
// //

View File

@ -223,7 +223,7 @@
<tr> <tr>
<td align=right width=100>Login token:</td> <td align=right width=100>Login token:</td>
<td> <td>
<input id=tokenInput type=text name=token maxlength=50 onkeyup=checkToken(event) onkeydown=checkToken(event) /> <input id=tokenInput type=text name=token maxlength=50 onchange=checkToken(event) onkeyup=checkToken(event) onkeydown=checkToken(event) />
<input id=hwtokenInput type=text name=hwtoken style="display:none" /> <input id=hwtokenInput type=text name=hwtoken style="display:none" />
</td> </td>
</tr> </tr>
@ -236,6 +236,28 @@
<hr /><a onclick=xgo(1) style=cursor:pointer>Back to login</a> <hr /><a onclick=xgo(1) style=cursor:pointer>Back to login</a>
</form> </form>
</div> </div>
<div id=resettokenpanel style="background-color: #979797;border-radius:16px;width:300px;padding:16px;text-align:center;display:none">
<form action=resetaccount method=post>
<div id=message5>
{{{message}}}
</div>
<table>
<tr>
<td align=right width=100>Login token:</td>
<td>
<input id=resetTokenInput type=text name=token maxlength=50 onchange=resetCheckToken(event) onkeyup=resetCheckToken(event) onkeydown=resetCheckToken(event) />
<input id=resetHwtokenInput type=text name=hwtoken style="display:none" />
</td>
</tr>
<tr>
<td colspan=2>
<div style=float:right><input id=resetTokenOkButton 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> </td>
</tr> </tr>
</table> </table>
@ -320,6 +342,19 @@
}, hardwareKeyChallenge.timeoutSeconds); }, hardwareKeyChallenge.timeoutSeconds);
} }
} }
if ('{{loginmode}}' == '5') {
try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null }
if ((hardwareKeyChallenge != null) && u2fSupported()) {
window.u2f.sign(hardwareKeyChallenge.appId, hardwareKeyChallenge.challenge, hardwareKeyChallenge.registeredKeys, function (authResponse) {
if (authResponse.signatureData) {
Q('resetHwtokenInput').value = JSON.stringify(authResponse);
QE('resetTokenOkButton', true);
Q('resetTokenOkButton').click();
}
}, hardwareKeyChallenge.timeoutSeconds);
}
}
} }
function showPassHint() { function showPassHint() {
@ -329,6 +364,9 @@
function xgo(x) { function xgo(x) {
QV('message1', false); QV('message1', false);
QV('message2', false); QV('message2', false);
QV('message3', false);
QV('message4', false);
QV('message5', false);
go(x); go(x);
} }
@ -339,6 +377,7 @@
QV('createpanel', x == 2); QV('createpanel', x == 2);
QV('resetpanel', x == 3); QV('resetpanel', x == 3);
QV('tokenpanel', x == 4); QV('tokenpanel', x == 4);
QV('resettokenpanel', x == 5);
if (x == 1) { Q('username').focus(); } if (x == 1) { Q('username').focus(); }
if (x == 2) { Q('ausername').focus(); } if (x == 2) { Q('ausername').focus(); }
if (x == 3) { Q('remail').focus(); } if (x == 3) { Q('remail').focus(); }
@ -448,6 +487,13 @@
QE('tokenOkButton', (Q('tokenInput').value.length == 6) || (Q('tokenInput').value.length == 8) || (Q('tokenInput').value.length == 44)); QE('tokenOkButton', (Q('tokenInput').value.length == 6) || (Q('tokenInput').value.length == 8) || (Q('tokenInput').value.length == 44));
} }
function resetCheckToken() {
var t1 = Q('resetTokenInput').value;
var t2 = t1.split(' ').join('');
if (t1 != t2) { Q('resetTokenInput').value = t2; }
QE('resetTokenOkButton', (Q('resetTokenInput').value.length == 6) || (Q('resetTokenInput').value.length == 8) || (Q('resetTokenInput').value.length == 44));
}
// //
// POPUP DIALOG // POPUP DIALOG
// //

View File

@ -320,7 +320,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
// Return true if this user has 2-step auth active // Return true if this user has 2-step auth active
function checkUserOneTimePasswordRequired(domain, user) { function checkUserOneTimePasswordRequired(domain, user) {
return (user.otpsecret) || (user.otphkeys && (user.otphkeys.length > 0)); return ((user.otpsecret != null) || ((user.otphkeys != null) && (user.otphkeys.length > 0)));
} }
// Check the 2-step auth token // Check the 2-step auth token
@ -377,7 +377,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
} }
// Return a U2F hardware key challenge // Return a U2F hardware key challenge
// TODO: Figure out how to support many U2F keys at the same time.
function getHardwareKeyChallenge(req, domain, user, func) { function getHardwareKeyChallenge(req, domain, user, func) {
if (req.session.u2fchallenge) { delete req.session.u2fchallenge; }; if (req.session.u2fchallenge) { delete req.session.u2fchallenge; };
if (user.otphkeys && (user.otphkeys.length > 0)) { if (user.otphkeys && (user.otphkeys.length > 0)) {
@ -419,7 +418,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
var user = obj.users[userid]; var user = obj.users[userid];
// Check if this user has 2-step login active // Check if this user has 2-step login active
if (checkUserOneTimePasswordRequired(req.domain, user)) { if (checkUserOneTimePasswordRequired(domain, user)) {
checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result) { checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result) {
if (result == false) { if (result == false) {
// 2-step auth is required, but the token is not present or not valid. // 2-step auth is required, but the token is not present or not valid.
@ -427,7 +426,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
req.session.loginmode = '4'; req.session.loginmode = '4';
req.session.tokenusername = xusername; req.session.tokenusername = xusername;
req.session.tokenpassword = xpassword; req.session.tokenpassword = xpassword;
req.session.tokenRetry = true;
res.redirect(domain.url); res.redirect(domain.url);
} else { } else {
// Login succesful // Login succesful
@ -465,6 +463,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
delete req.session.loginmode; delete req.session.loginmode;
delete req.session.tokenusername; delete req.session.tokenusername;
delete req.session.tokenpassword; delete req.session.tokenpassword;
delete req.session.tokenemail;
delete req.session.success; delete req.session.success;
delete req.session.error; delete req.session.error;
delete req.session.passhint; delete req.session.passhint;
@ -503,7 +502,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if ((domain.newaccounts === 0) || (domain.newaccounts === false)) { res.sendStatus(401); return; } if ((domain.newaccounts === 0) || (domain.newaccounts === false)) { res.sendStatus(401); return; }
// Check if we exceed the maximum number of user accounts // Check if we exceed the maximum number of user accounts
obj.db.isMaxType(domain.maxaccounts, 'user', function (maxExceed) { obj.db.isMaxType(domain.limits.maxuseraccounts, 'user', domain.id, function (maxExceed) {
if (maxExceed) { if (maxExceed) {
req.session.loginmode = 2; req.session.loginmode = 2;
req.session.error = '<b style=color:#8C001A>Account limit reached.</b>'; req.session.error = '<b style=color:#8C001A>Account limit reached.</b>';
@ -569,28 +568,58 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
const domain = checkUserIpAddress(req, res); const domain = checkUserIpAddress(req, res);
if ((domain == null) || (domain.auth == 'sspi')) return; if ((domain == null) || (domain.auth == 'sspi')) return;
var email = req.body.email;
if ((email == null) || (email == '')) { email = req.session.tokenemail; }
if ((domain.newaccounts === 0) || (domain.newaccounts === false)) { res.sendStatus(401); return; } if ((domain.newaccounts === 0) || (domain.newaccounts === false)) { res.sendStatus(401); return; }
if (!req.body.email || checkEmail(req.body.email) == false) { if (!email || checkEmail(email) == false) {
req.session.loginmode = 3; req.session.loginmode = 3;
req.session.error = '<b style=color:#8C001A>Invalid email.</b>'; req.session.error = '<b style=color:#8C001A>Invalid email.</b>';
res.redirect(domain.url); res.redirect(domain.url);
} else { } else {
obj.db.GetUserWithVerifiedEmail(domain.id, req.body.email, function (err, docs) { obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
if (docs.length == 0) { if ((err != null) || (docs.length == 0)) {
req.session.loginmode = 3; req.session.loginmode = 3;
req.session.error = '<b style=color:#8C001A>Account not found.</b>'; req.session.error = '<b style=color:#8C001A>Account not found.</b>';
res.redirect(domain.url); res.redirect(domain.url);
} else { } else {
var userFound = docs[0]; var user = docs[0];
if (obj.parent.mailserver != null) { if (checkUserOneTimePasswordRequired(domain, user) == true) {
obj.parent.mailserver.sendAccountResetMail(domain, userFound.name, userFound.email); // Second factor setup, request it now.
req.session.loginmode = 1; checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result) {
req.session.error = '<b style=color:darkgreen>Hold on, reset mail sent.</b>'; if (result == false) {
res.redirect(domain.url); // 2-step auth is required, but the token is not present or not valid.
if ((req.body.token != null) || (req.body.hwtoken != null)) { req.session.error = '<b style=color:#8C001A>Invalid token, try again.</b>'; }
req.session.loginmode = '5';
req.session.tokenemail = email;
res.redirect(domain.url);
} else {
// Send email to perform recovery.
delete req.session.tokenemail;
if (obj.parent.mailserver != null) {
obj.parent.mailserver.sendAccountResetMail(domain, user.name, user.email);
req.session.loginmode = 1;
req.session.error = '<b style=color:darkgreen>Hold on, reset mail sent.</b>';
res.redirect(domain.url);
} else {
req.session.loginmode = 3;
req.session.error = '<b style=color:#8C001A>Unable to sent email.</b>';
res.redirect(domain.url);
}
}
});
} else { } else {
req.session.loginmode = 3; // No second factor, send email to perform recovery.
req.session.error = '<b style=color:#8C001A>Unable to sent email.</b>'; if (obj.parent.mailserver != null) {
res.redirect(domain.url); obj.parent.mailserver.sendAccountResetMail(domain, user.name, user.email);
req.session.loginmode = 1;
req.session.error = '<b style=color:darkgreen>Hold on, reset mail sent.</b>';
res.redirect(domain.url);
} else {
req.session.loginmode = 3;
req.session.error = '<b style=color:#8C001A>Unable to sent email.</b>';
res.redirect(domain.url);
}
} }
} }
}); });
@ -632,7 +661,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
obj.db.SetUser(user); obj.db.SetUser(user);
// Event the change // Event the change
obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, { etype: 'user', username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Verified email of user ' + EscapeHtml(user.name) + ' (' + EscapeHtml(userinfo.email) + ')', domain: domain.id }); obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, { etype: 'user', username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Verified email of user ' + EscapeHtml(user.name) + ' (' + EscapeHtml(user.email) + ')', domain: domain.id });
// Send the confirmation page // Send the confirmation page
res.render(obj.path.join(obj.parent.webViewsPath, 'message'), { title: domain.title, title2: domain.title2, title3: 'Account Verification', message: 'Verified email <b>' + EscapeHtml(user.email) + '</b> for user account <b>' + EscapeHtml(user.name) + '</b>. <a href="' + domain.url + '">Go to login page</a>.' }); res.render(obj.path.join(obj.parent.webViewsPath, 'message'), { title: domain.title, title2: domain.title2, title3: 'Account Verification', message: 'Verified email <b>' + EscapeHtml(user.email) + '</b> for user account <b>' + EscapeHtml(user.name) + '</b>. <a href="' + domain.url + '">Go to login page</a>.' });
@ -660,7 +689,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
userinfo.hash = hash; userinfo.hash = hash;
userinfo.passchange = Math.floor(Date.now() / 1000); userinfo.passchange = Math.floor(Date.now() / 1000);
userinfo.passhint = null; userinfo.passhint = null;
delete userinfo.otpsecret; // Currently a email password reset will turn off 2-step login. //delete userinfo.otpsecret; // Currently a email password reset will turn off 2-step login.
obj.db.SetUser(userinfo); obj.db.SetUser(userinfo);
// Event the change // Event the change
@ -920,6 +949,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
} else { } else {
// Send back the login application // Send back the login application
// If this is a 2 factor auth request, look for a hardware key challenge. // If this is a 2 factor auth request, look for a hardware key challenge.
// Normal login 2 factor request
if ((req.session.loginmode == '4') && (req.session.tokenusername)) { if ((req.session.loginmode == '4') && (req.session.tokenusername)) {
var user = obj.users['user/' + domain.id + '/' + req.session.tokenusername]; var user = obj.users['user/' + domain.id + '/' + req.session.tokenusername];
if (user != null) { if (user != null) {
@ -927,6 +957,24 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
return; return;
} }
} }
// Password recovery 2 factor request
if ((req.session.loginmode == '5') && (req.session.tokenemail)) {
obj.db.GetUserWithVerifiedEmail(domain.id, req.session.tokenemail, function (err, docs) {
if ((err != null) || (docs.length == 0)) {
req.session = null;
res.redirect(domain.url);
} else {
var user = obj.users[docs[0]._id];
if (user != null) {
getHardwareKeyChallenge(req, domain, user, function (u2fChallenge) { handleRootRequestLogin(req, res, domain, u2fChallenge, passRequirements); });
} else {
req.session = null;
res.redirect(domain.url);
}
}
});
return;
}
handleRootRequestLogin(req, res, domain, '', passRequirements); handleRootRequestLogin(req, res, domain, '', passRequirements);
} }
} }