From 48d554a05dee7014b91943e92834085c9d76ac7a Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Thu, 11 Apr 2019 13:41:51 -0700 Subject: [PATCH] Added LDAP support. --- common.js | 5 +- meshcentral.js | 23 +++++-- meshuser.js | 10 +-- package.json | 1 + views/default-min.handlebars | 2 +- views/default.handlebars | 8 +-- webserver.js | 119 ++++++++++++++++++++++------------- 7 files changed, 108 insertions(+), 60 deletions(-) diff --git a/common.js b/common.js index 2db7f60b..bac385b6 100644 --- a/common.js +++ b/common.js @@ -128,10 +128,11 @@ module.exports.escapeHtml = function (string) { return String(string).replace(/[ module.exports.escapeHtmlBreaks = function (string) { return String(string).replace(/[&<>"'`=\/]/g, function (s) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/', '`': '`', '=': '=', '\r': '
', '\n': '' }[s]; }); }; // Lowercase all the names in a object recursively -module.exports.objKeysToLower = function (obj) { +// Allow for exception keys, child of exceptions will not get lower-cased. +module.exports.objKeysToLower = function (obj, exceptions) { for (var i in obj) { if (i.toLowerCase() !== i) { obj[i.toLowerCase()] = obj[i]; delete obj[i]; } // LowerCase all key names - if (typeof obj[i] == 'object') { module.exports.objKeysToLower(obj[i]); } // LowerCase all key names in the child object + if ((typeof obj[i] == 'object') && ((exceptions == null) || (exceptions.indexOf(i.toLowerCase()) == -1))) { module.exports.objKeysToLower(obj[i], exceptions); } // LowerCase all key names in the child object } return obj; }; diff --git a/meshcentral.js b/meshcentral.js index 7584a7bc..dbe2bc60 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -479,7 +479,7 @@ function CreateMeshCentralServer(config, args) { // Lower case all keys in the config file try { - require('./common.js').objKeysToLower(config2); + require('./common.js').objKeysToLower(config2, ["ldapoptions"]); } catch (ex) { console.log('CRITICAL ERROR: Unable to access the file \"./common.js\".\r\nCheck folder & file permissions.'); process.exit(); @@ -543,8 +543,9 @@ function CreateMeshCentralServer(config, args) { if (obj.config.domains[''].dns != null) { console.log("ERROR: Default domain can't have a DNS name."); return; } var xdomains = {}; for (i in obj.config.domains) { if (obj.config.domains[i].title == null) { obj.config.domains[i].title = 'MeshCentral'; } if (obj.config.domains[i].title2 == null) { obj.config.domains[i].title2 = '2.0 Beta 2'; } xdomains[i.toLowerCase()] = obj.config.domains[i]; } obj.config.domains = xdomains; 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 config.json."); return; } } } for (i in obj.config.domains) { + if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); } 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 = '/'; } obj.config.domains[i].id = i; @@ -552,6 +553,11 @@ function CreateMeshCentralServer(config, args) { if (typeof obj.config.domains[i].userblockedip == 'string') { if (obj.config.domains[i].userblockedip == '') { obj.config.domains[i].userblockedip = null; } else { obj.config.domains[i].userblockedip = obj.config.domains[i].userallowedip.split(','); } } if (typeof obj.config.domains[i].agentallowedip == 'string') { if (obj.config.domains[i].agentallowedip == '') { obj.config.domains[i].agentallowedip = null; } else { obj.config.domains[i].agentallowedip = obj.config.domains[i].agentallowedip.split(','); } } if (typeof obj.config.domains[i].agentblockedip == 'string') { if (obj.config.domains[i].agentblockedip == '') { obj.config.domains[i].agentblockedip = null; } else { obj.config.domains[i].agentblockedip = obj.config.domains[i].agentblockedip.split(','); } } + if ((obj.config.domains[i].auth == 'ldap') && (typeof obj.config.domains[i].ldapoptions != 'object')) { + if (i == '') { console.log("ERROR: Default domain is LDAP, but is missing LDAPOptions."); } else { console.log("ERROR: Domain '" + i + "' is LDAP, but is missing LDAPOptions."); } + process.exit(); + return; + } } // Log passed arguments into Windows Service Log @@ -1618,11 +1624,12 @@ function getConfig(createSampleConfig) { // Lower case all keys in the config file try { - require('./common.js').objKeysToLower(config); + require('./common.js').objKeysToLower(config, ["ldapoptions"]); } catch (ex) { console.log('CRITICAL ERROR: Unable to access the file \"./common.js\".\r\nCheck folder & file permissions.'); process.exit(); } + return config; } @@ -1680,16 +1687,24 @@ function mainStart(args) { var config = getConfig(false); if (config == null) { process.exit(); } + // Lowercase the auth value is present + for (var i in config.domains) { if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); } } + // Check is Windows SSPI and YubiKey OTP will be used var sspi = false; + var ldap = false; var allsspi = true; var yubikey = false; if (require('os').platform() == 'win32') { for (var i in config.domains) { if (config.domains[i].auth == 'sspi') { sspi = true; } else { allsspi = false; } } } else { allsspi = false; } - for (var i in config.domains) { if (config.domains[i].yubikey != null) { yubikey = true; } } + for (var i in config.domains) { + if (config.domains[i].yubikey != null) { yubikey = true; } + if (config.domains[i].auth == 'ldap') { ldap = true; } + } // Build the list of required modules var modules = ['ws', 'nedb', 'https', 'yauzl', 'xmldom', 'express', 'archiver', 'multiparty', 'node-forge', 'express-ws', 'compression', 'body-parser', 'connect-redis', 'express-handlebars']; if (require('os').platform() == 'win32') { modules.push('node-windows'); if (sspi == true) { modules.push('node-sspi'); } } // Add Windows modules + if (ldap == true) { modules.push('ldapauth-fork'); } if (config.letsencrypt != null) { modules.push('greenlock'); modules.push('le-store-certbot'); modules.push('le-challenge-fs'); modules.push('le-acme-core'); } // Add Greenlock Modules if (config.settings.mongodb != null) { modules.push('mongojs'); } // Add MongoDB if (config.smtp != null) { modules.push('nodemailer'); } // Add SMTP support diff --git a/meshuser.js b/meshuser.js index d659f457..1248dc41 100644 --- a/meshuser.js +++ b/meshuser.js @@ -251,7 +251,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use var httpport = ((args.aliasport != null) ? args.aliasport : args.port); // Build server information object - var serverinfo = { name: parent.certificates.CommonName, mpsname: parent.certificates.AmtMpsName, mpsport: mpsport, mpspass: args.mpspass, port: httpport, emailcheck: ((parent.parent.mailserver != null) && (domain.auth != 'sspi')), domainauth: (domain.auth == 'sspi') }; + var serverinfo = { name: parent.certificates.CommonName, mpsname: parent.certificates.AmtMpsName, mpsport: mpsport, mpspass: args.mpspass, port: httpport, emailcheck: ((parent.parent.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap')), domainauth: ((domain.auth == 'sspi') || (domain.auth == 'ldap')) }; if (args.notls == true) { serverinfo.https = false; } else { serverinfo.https = true; serverinfo.redirport = args.redirport; } // Send server information @@ -715,7 +715,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use case 'changeemail': { // Change the email address - if (domain.auth == 'sspi') return; + if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) return; if (common.validateEmail(command.email, 1, 256) == false) return; if (parent.users[req.session.userid].email != command.email) { // Check if this email is already validated on a different account @@ -740,7 +740,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use parent.parent.DispatchEvent(['*', 'server-users', user._id], obj, message); // Send the verification email - if ((parent.parent.mailserver != null) && (domain.auth != 'sspi')) { parent.parent.mailserver.sendAccountCheckMail(domain, user.name, user.email); } + if (parent.parent.mailserver != null) { parent.parent.mailserver.sendAccountCheckMail(domain, user.name, user.email); } } }); } @@ -749,7 +749,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use case 'verifyemail': { // Send a account email verification email - if (domain.auth == 'sspi') return; + if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) return; if (common.validateString(command.email, 3, 1024) == false) return; if ((parent.parent.mailserver != null) && (parent.users[req.session.userid].email == command.email)) { // Send the verification email @@ -1069,7 +1069,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 64) != 0)) break; // In some situations, we need a verified email address to create a device group. - if ((parent.parent.mailserver != null) && (domain.auth != 'sspi') && (user.emailVerified !== true) && (user.siteadmin != 0xFFFFFFFF)) return; // User must verify it's email first. + if ((parent.parent.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (user.emailVerified !== true) && (user.siteadmin != 0xFFFFFFFF)) return; // User must verify it's email first. // Create mesh if (common.validateString(command.meshname, 1, 64) == false) break; // Meshname is between 1 and 64 characters diff --git a/package.json b/package.json index 41c0bb0d..83a01954 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "express-handlebars": "^3.0.0", "express-ws": "^4.0.0", "ipcheck": "^0.1.0", + "ldapauth-fork": "^4.2.0", "meshcentral": "*", "minimist": "^1.2.0", "multiparty": "^4.2.1", diff --git a/views/default-min.handlebars b/views/default-min.handlebars index 6a8ead42..280d7e91 100644 --- a/views/default-min.handlebars +++ b/views/default-min.handlebars @@ -1 +1 @@ - {{{title}}}
{{{title}}}
{{{title2}}}

{{{logoutControl}}}

 

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

{{{logoutControl}}}

 

\ No newline at end of file diff --git a/views/default.handlebars b/views/default.handlebars index 78b24ca9..36f6452c 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -73,13 +73,13 @@
-
+ -
+ - @@ -1663,7 +1663,7 @@ } } masterUpdate(4 + 128); - if (currentNode && (currentNode.meshid == message.event.meshid)) { refreshDevice(currentNode._id); } + if (currentNode && (currentNode.meshid == message.event.meshid)) { currentNode = null; if ((xxcurrentView >= 10) && (xxcurrentView < 20)) { go(1); } } //meshserver.send({ action: 'files' }); // TODO: Why do we need to do this?? // If we are looking at a mesh that is now deleted, move back to "My Account" diff --git a/webserver.js b/webserver.js index 87d85d0e..c733515d 100644 --- a/webserver.js +++ b/webserver.js @@ -217,35 +217,70 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { obj.authenticate = function (name, pass, domain, fn) { if ((typeof (name) != 'string') || (typeof (pass) != 'string') || (typeof (domain) != 'object')) { fn(new Error('invalid fields')); return; } if (!module.parent) console.log('authenticating %s:%s:%s', domain.id, name, pass); - var user = obj.users['user/' + domain.id + '/' + name.toLowerCase()]; - // Query the db for the given username - if (!user) { fn(new Error('cannot find user')); return; } - // Apply the same algorithm to the POSTed password, applying the hash against the pass / salt, if there is a match we found the user - if (user.salt == null) { - fn(new Error('invalid password')); + + if (domain.auth == 'ldap') { + // LDAP login + var LdapAuth = require('ldapauth-fork'); + var ldap = new LdapAuth(domain.ldapoptions); + ldap.authenticate(name, pass, function (err, xxuser) { + try { ldap.close(); } catch (ex) { console.log(ex); } // Close the LDAP object + if (err) { fn(new Error('invalid password')); return; } + if (xxuser.objectSid == null) { fn(new Error('no objectSid')); return; } + var userid = 'user/' + domain.id + '/' + Buffer.from(xxuser.objectSid, 'binary').toString('hex').toLowerCase(); + var user = obj.users[userid]; + if (user == null) { + // This user does not exist, create a new account. + var name = null; + if (xxuser.displayName) { name = xxuser.displayName; } + else if (xxuser.name) { name = xxuser.name; } + else if (xxuser.cn) { name = xxuser.cn; } + if (name == null) { fn(new Error('no user name')); return; } + var user = { type: 'user', _id: userid, name: name, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), domain: domain.id }; + var usercount = 0; + for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } } + if (usercount == 0) { user.siteadmin = 0xFFFFFFFF; if (domain.newaccounts === 2) { domain.newaccounts = 0; } } // If this is the first user, give the account site admin. + obj.users[user._id] = user; + obj.db.SetUser(user); + obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msg: 'Account created, name is ' + name, domain: domain.id }); + return fn(null, user._id); + } else { + // This is an existing user + if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; } + return fn(null, user._id); + } + }); } else { - if (user.passtype != null) { - // IIS default clear or weak password hashing (SHA-1) - require('./pass').iishash(user.passtype, pass, user.salt, function (err, hash) { - if (err) return fn(err); - if (hash == user.hash) { - // Update the password to the stronger format. - require('./pass').hash(pass, function (err, salt, hash) { if (err) throw err; user.salt = salt; user.hash = hash; delete user.passtype; obj.db.SetUser(user); }); - if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; } - return fn(null, user._id); - } - fn(new Error('invalid password'), null, user.passhint); - }); + // Regular login + var user = obj.users['user/' + domain.id + '/' + name.toLowerCase()]; + // Query the db for the given username + if (!user) { fn(new Error('cannot find user')); return; } + // Apply the same algorithm to the POSTed password, applying the hash against the pass / salt, if there is a match we found the user + if (user.salt == null) { + fn(new Error('invalid password')); } else { - // Default strong password hashing (pbkdf2 SHA384) - require('./pass').hash(pass, user.salt, function (err, hash) { - if (err) return fn(err); - if (hash == user.hash) { - if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; } - return fn(null, user._id); - } - fn(new Error('invalid password'), null, user.passhint); - }); + if (user.passtype != null) { + // IIS default clear or weak password hashing (SHA-1) + require('./pass').iishash(user.passtype, pass, user.salt, function (err, hash) { + if (err) return fn(err); + if (hash == user.hash) { + // Update the password to the stronger format. + require('./pass').hash(pass, function (err, salt, hash) { if (err) throw err; user.salt = salt; user.hash = hash; delete user.passtype; obj.db.SetUser(user); }); + if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; } + return fn(null, user._id); + } + fn(new Error('invalid password'), null, user.passhint); + }); + } else { + // Default strong password hashing (pbkdf2 SHA384) + require('./pass').hash(pass, user.salt, function (err, hash) { + if (err) return fn(err); + if (hash == user.hash) { + if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; } + return fn(null, user._id); + } + fn(new Error('invalid password'), null, user.passhint); + }); + } } } }; @@ -611,7 +646,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { function handleCreateAccountRequest(req, res) { const domain = checkUserIpAddress(req, res); - if ((domain == null) || (domain.auth == 'sspi')) return; + if ((domain == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap')) { res.sendStatus(401); return; } if ((domain.newaccounts === 0) || (domain.newaccounts === false)) { res.sendStatus(401); return; } @@ -684,8 +719,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { obj.db.SetUser(user); // Send the verification email - if ((obj.parent.mailserver != null) && (domain.auth != 'sspi') && (obj.common.validateEmail(user.email, 1, 256) == true)) { obj.parent.mailserver.sendAccountCheckMail(domain, user.name, user.email); } - + if ((obj.parent.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (obj.common.validateEmail(user.email, 1, 256) == true)) { obj.parent.mailserver.sendAccountCheckMail(domain, user.name, user.email); } }); obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msg: 'Account created, email is ' + req.body.email, domain: domain.id }); } @@ -702,7 +736,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { 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')) { + if ((domain == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap') || (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; @@ -776,7 +810,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Called to process an account reset request function handleResetAccountRequest(req, res) { const domain = checkUserIpAddress(req, res); - if ((domain == null) || (domain.auth == 'sspi')) return; + if ((domain == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap')) { res.sendStatus(401); return; } // Get the email from the body or session. var email = req.body.email; @@ -840,7 +874,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Called to process a web based email verification request function handleCheckMailRequest(req, res) { const domain = checkUserIpAddress(req, res); - if ((domain == null) || (domain.auth == 'sspi')) return; + if ((domain == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap')) { res.sendStatus(401); return; } if (req.query.c != null) { var cookie = obj.parent.decodeCookie(req.query.c, obj.parent.mailserver.mailCookieEncryptionKey, 30); @@ -927,7 +961,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { function handleDeleteAccountRequest(req, res) { const domain = checkUserIpAddress(req, res); - if ((domain == null) || (domain.auth == 'sspi')) return; + if ((domain == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap')) { res.sendStatus(401); return; } // Check if the user is logged and we have all required parameters if (!req.session || !req.session.userid || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.domainid != domain.id)) { res.redirect(domain.url); return; } @@ -998,7 +1032,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Handle password changes function handlePasswordChangeRequest(req, res) { const domain = checkUserIpAddress(req, res); - if ((domain == null) || (domain.auth == 'sspi')) return; + if ((domain == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap')) { res.sendStatus(401); return; } // Check if the user is logged and we have all required parameters if (!req.session || !req.session.userid || !req.body.apassword0 || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.domainid != domain.id)) { res.redirect(domain.url); return; } @@ -1038,15 +1072,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { domain.sspi.authenticate(req, res, function (err) { if ((err != null) || (req.connection.user == null)) { res.end('Authentication Required...'); } else { handleRootRequestEx(req, res, domain); } }); } else if (req.query.user && req.query.pass) { // User credentials are being passed in the URL. WARNING: Putting credentials in a URL is not good security... but people are requesting this option. - var userid = 'user/' + domain.id + '/' + req.query.user.toLowerCase(); - if (obj.users[userid] != null) { - obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid) { - req.session.userid = userid; - req.session.domainid = domain.id; - req.session.currentNode = ''; - handleRootRequestEx(req, res, domain); - }); - } + obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid) { + req.session.userid = userid; + req.session.domainid = domain.id; + req.session.currentNode = ''; + handleRootRequestEx(req, res, domain); + }); } else { // Login using a different system handleRootRequestEx(req, res, domain);