diff --git a/LesionTracker/client/routes.js b/LesionTracker/client/routes.js index c0b4a3a80..610e03f02 100644 --- a/LesionTracker/client/routes.js +++ b/LesionTracker/client/routes.js @@ -36,7 +36,7 @@ var routerOptions = { Router.route('/', function() { // Check user is logged in if(Meteor.user() && Meteor.userId()) { - if (!Meteor.user().emails[0].verified && verifyEmail) { + if (verifyEmail && Meteor.user().emails && !Meteor.user().emails[0].verified) { this.render('emailVerification', routerOptions); } else { this.render('worklist', routerOptions); @@ -50,7 +50,7 @@ Router.route('/', function() { Router.route('/worklist', function() { // Check user is logged in if(Meteor.user() && Meteor.userId()) { - if (!Meteor.user().emails[0].verified && verifyEmail) { + if (verifyEmail && Meteor.user().emails && !Meteor.user().emails[0].verified) { this.render('emailVerification', routerOptions); } else { this.render('worklist', routerOptions); diff --git a/Packages/accounts-ldap/ldap_client.js b/Packages/accounts-ldap/ldap_client.js new file mode 100644 index 000000000..c4c65e758 --- /dev/null +++ b/Packages/accounts-ldap/ldap_client.js @@ -0,0 +1,43 @@ +// Pass in username, password as normal +// customLdapOptions should be passed in if you want to override LDAP_DEFAULTS +// on any particular call (if you have multiple ldap servers you'd like to connect to) +// You'll likely want to set the dn value here {dn: "..."} +Meteor.loginWithLDAP = function (user, password, customLdapOptions, callback) { + // Retrieve arguments as array + var args = []; + for (var i = 0; i < arguments.length; i++) { + args.push(arguments[i]); + } + // Pull username and password + user = args.shift(); + password = args.shift(); + + // Check if last argument is a function + // if it is, pop it off and set callback to it + if (typeof args[args.length - 1] == 'function') callback = args.pop(); else callback = null; + + // if args still holds options item, grab it + if (args.length > 0) customLdapOptions = args.shift(); else customLdapOptions = {}; + + // Set up loginRequest object + var loginRequest = _.defaults({ + username: user, + ldapPass: password + }, { + ldap: true, + ldapOptions: customLdapOptions + }); + + Accounts.callLoginMethod({ + // Call login method with ldap = true + // This will hook into our login handler for ldap + methodArguments: [loginRequest], + userCallback: function (error, result) { + if (error) { + callback && callback(error); + } else { + callback && callback(); + } + } + }); +}; \ No newline at end of file diff --git a/Packages/accounts-ldap/ldap_server.js b/Packages/accounts-ldap/ldap_server.js new file mode 100644 index 000000000..66a67b15c --- /dev/null +++ b/Packages/accounts-ldap/ldap_server.js @@ -0,0 +1,366 @@ +Future = Npm.require('fibers/future'); + +// At a minimum, set up LDAP_DEFAULTS.url and .dn according to +// your needs. url should appear as 'ldap://your.url.here' +// dn should appear in normal ldap format of comma separated attribute=value +// e.g. 'uid=someuser,cn=users,dc=somevalue' +LDAP_DEFAULTS = { + url: false, + port: '389', + dn: false, + searchDN: false, + searchCredentials: false, + createNewUser: true, + defaultDomain: false, + searchResultsProfileMap: false, + base: null, + search: '(objectclass=*)', + ldapsCertificate: false +}; + +/** + @class LDAP + @constructor + */ +var LDAP = function (options) { + // Set options + this.options = _.defaults(options, LDAP_DEFAULTS); + + // Make sure options have been set + try { + check(this.options.url, String); + //check(this.options.dn, String); + } catch (e) { + throw new Meteor.Error('Bad Defaults', 'Options not set. Make sure to set LDAP_DEFAULTS.url and LDAP_DEFAULTS.dn!'); + } + + // Because NPM ldapjs module has some binary builds, + // We had to create a wraper package for it and build for + // certain architectures. The package typ:ldap-js exports + // 'MeteorWrapperLdapjs' which is a wrapper for the npm module + this.ldapjs = MeteorWrapperLdapjs; +}; + +/** + * Attempt to bind (authenticate) ldap + * and perform a dn search if specified + * + * @method ldapCheck + * + * @param {Object} options Object with username, ldapPass and overrides for LDAP_DEFAULTS object. + * Additionally the searchBeforeBind parameter can be specified, which is used to search for the DN + * if not provided. + */ +LDAP.prototype.ldapCheck = function (options) { + + var self = this; + + options = options || {}; + + if (options.hasOwnProperty('username') && options.hasOwnProperty('ldapPass')) { + + var ldapAsyncFut = new Future(); + + + // Create ldap client + var fullUrl = self.options.url + ':' + self.options.port; + var client = null; + + if (self.options.url.indexOf('ldaps://') === 0) { + client = self.ldapjs.createClient({ + url: fullUrl, + tlsOptions: { + ca: [self.options.ldapsCertificate] + } + }); + } + else { + client = self.ldapjs.createClient({ + url: fullUrl + }); + } + + + // Slide @xyz.whatever from username if it was passed in + // and replace it with the domain specified in defaults + var emailSliceIndex = options.username.indexOf('@'); + var username; + var domain = self.options.defaultDomain; + + // If user appended email domain, strip it out + // And use the defaults.defaultDomain if set + if (emailSliceIndex !== -1) { + username = options.username.substring(0, emailSliceIndex); + domain = domain || options.username.substring((emailSliceIndex + 1), options.username.length); + } else { + username = options.username; + } + + + // If DN is provided, use it to bind + if (self.options.dn) { + // Attempt to bind to ldap server with provided info + client.bind(self.options.searchDN || self.options.dn, self.options.searchCredentials || options.ldapPass, function (err) { + try { + if (err) { + // Bind failure, return error + throw new Meteor.Error(err.code, err.message); + } + + var handleSearchProfile = function (retObject, bindAfterSearch) { + retObject.emptySearch = true; + + // construct list of ldap attributes to fetch + var attributes = []; + if (self.options.searchResultsProfileMap) { + self.options.searchResultsProfileMap.map(function (item) { + attributes.push(item.resultKey); + }); + } + + // use base if given, else the dn for the ldap search + var searchBase = self.options.base || self.options.dn; + var searchOptions = { + scope: 'sub', + sizeLimit: 1, + attributes: attributes, + filter: self.options.search + }; + + client.search(searchBase, searchOptions, function (err, res) { + if (err) { + ldapAsyncFut.return({ + error: err + }); + } + + res.on('searchEntry', function (entry) { + retObject.emptySearch = false; + // Add entry results to return object + retObject.searchResults = entry.object; + if (bindAfterSearch) { + client.bind(retObject.searchResults.dn, options.ldapPass, function (err) { + try { + if (err) { + throw new Meteor.Error(err.code, err.message); + } + ldapAsyncFut.return(retObject); + } catch (e) { + ldapAsyncFut.return({ + error: e + }); + } + }) + } else ldapAsyncFut.return(retObject); + }); + + // This call causes Future issue: "Future resolved more than once" + // Because it is already called in searchEntry handler + /*res.on('end', function () { + ldapAsyncFut.return(retObject); + });*/ + + }); + }; + + var retObject = { + username: username, + searchResults: null, + email: domain ? username + '@' + domain : false + }; + + if (self.options.searchDN) { + handleSearchProfile(retObject, true); + } else if (self.options.searchResultsProfileMap) { + handleSearchProfile(retObject, false); + } else { + ldapAsyncFut.return(retObject); + } + + } catch (e) { + ldapAsyncFut.return({ + error: e + }); + } + }); + } + // DN not provided, search for DN and use result to bind + else if (typeof self.options.searchBeforeBind !== undefined) { + // initialize result + var retObject = { + username: username, + email: domain ? username + '@' + domain : false, + emptySearch: true, + searchResults: {} + }; + + // compile attribute list to return + var searchAttributes = ['dn']; + self.options.searchResultsProfileMap.map(function (item) { + searchAttributes.push(item.resultKey); + }); + + + var filter = self.options.search; + Object.keys(options.ldapOptions.searchBeforeBind).forEach(function (searchKey) { + filter = '&' + filter + '(' + searchKey + '=' + options.ldapOptions.searchBeforeBind[searchKey] + ')'; + }); + var searchOptions = { + scope: 'sub', + sizeLimit: 1, + filter: filter + }; + + // perform LDAP search to determine DN + client.search(self.options.base, searchOptions, function (err, res) { + retObject.emptySearch = true; + res.on('searchEntry', function (entry) { + retObject.dn = entry.objectName; + retObject.username = retObject.dn; + retObject.emptySearch = false; + + // Return search results if specified + if (self.options.searchResultsProfileMap) { + // construct list of ldap attributes to fetch + self.options.searchResultsProfileMap.map(function (item) { + retObject.searchResults[item.resultKey] = entry.object[item.resultKey]; + }); + } + + // use the determined DN to bind + client.bind(entry.objectName, options.ldapPass, function (err) { + try { + if (err) { + throw new Meteor.Error(err.code, err.message); + } + else { + ldapAsyncFut.return(retObject); + } + } + catch (e) { + ldapAsyncFut.return({ + error: e + }); + } + }); + }); + // If no dn is found, return as is. + res.on('end', function (result) { + if (retObject.dn === undefined) { + ldapAsyncFut.return(retObject); + } + }); + }); + } + + return ldapAsyncFut.wait(); + + } else { + throw new Meteor.Error(403, 'Missing LDAP Auth Parameter'); + } + +}; + + +// Register login handler with Meteor +// Here we create a new LDAP instance with options passed from +// Meteor.loginWithLDAP on client side +// @param {Object} loginRequest will consist of username, ldapPass, ldap, and ldapOptions +Accounts.registerLoginHandler('ldap', function (loginRequest) { + // If 'ldap' isn't set in loginRequest object, + // then this isn't the proper handler (return undefined) + if (!loginRequest.ldap) { + return undefined; + } + + // Instantiate LDAP with options + var userOptions = loginRequest.ldapOptions || {}; + var ldapObj = new LDAP(userOptions); + + // Call ldapCheck and get response + var ldapResponse = ldapObj.ldapCheck(loginRequest); + + if (ldapResponse.error) { + return { + userId: null, + error: ldapResponse.error + }; + } + else if (ldapResponse.emptySearch) { + return { + userId: null, + error: new Meteor.Error(403, 'User not found in LDAP') + }; + } + else { + // Set initial userId and token vals + var userId = null; + var stampedToken = { + token: null + }; + + // Look to see if user already exists + var user = Meteor.users.findOne({ + username: ldapResponse.username + }); + + // Login user if they exist + if (user) { + userId = user._id; + + // Create hashed token so user stays logged in + stampedToken = Accounts._generateStampedLoginToken(); + var hashStampedToken = Accounts._hashStampedToken(stampedToken); + // Update the user's token in mongo + Meteor.users.update(userId, { + $push: { + 'services.resume.loginTokens': hashStampedToken + } + }); + } + // Otherwise create user if option is set + else if (ldapObj.options.createNewUser) { + var userObject = { + username: ldapResponse.username + }; + // Set email + if (ldapResponse.email) userObject.email = ldapResponse.email; + + // Set profile values if specified in searchResultsProfileMap + if (ldapResponse.searchResults && ldapObj.options.searchResultsProfileMap.length > 0) { + + var profileMap = ldapObj.options.searchResultsProfileMap; + var profileObject = {}; + + // Loop through profileMap and set values on profile object + for (var i = 0; i < profileMap.length; i++) { + var resultKey = profileMap[i].resultKey; + + // If our search results have the specified property, set the profile property to its value + if (ldapResponse.searchResults.hasOwnProperty(resultKey)) { + profileObject[profileMap[i].profileProperty] = ldapResponse.searchResults[resultKey]; + } + + } + // Set userObject profile + userObject.profile = profileObject; + } + + + userId = Accounts.createUser(userObject); + } else { + // Ldap success, but no user created + console.log('LDAP Authentication succeeded for ' + ldapResponse.username + ', but no user exists in Meteor. Either create the user manually or set LDAP_DEFAULTS.createNewUser to true'); + return { + userId: null, + error: new Meteor.Error(403, 'User found in LDAP but not in application') + }; + } + + return { + userId: userId, + token: stampedToken.token + }; + } + +}); \ No newline at end of file diff --git a/Packages/accounts-ldap/package.js b/Packages/accounts-ldap/package.js new file mode 100644 index 000000000..30fe0d7bf --- /dev/null +++ b/Packages/accounts-ldap/package.js @@ -0,0 +1,28 @@ +Package.describe({ + name: 'typ:accounts-ldap', + version: '1.0.1', + summary: 'Accounts login for LDAP using ldapjs. Supports anonymous DN search & LDAPS.', + git: 'https://github.com/typ90/meteor-accounts-ldap', + documentation: 'README.md' +}); + + +Package.onUse(function(api) { + api.versionsFrom('1.0.3.1'); + + api.use(['templating'], 'client'); + api.use(['typ:ldapjs@0.7.3'], 'server'); + + + api.use('accounts-base', 'server'); + api.imply('accounts-base'); + api.imply('accounts-password'); + + api.use('check'); + + api.addFiles(['ldap_client.js'], 'client'); + api.addFiles(['ldap_server.js'], 'server'); + + api.export('LDAP', 'server'); + api.export('LDAP_DEFAULTS', 'server'); +}); \ No newline at end of file diff --git a/Packages/active-entry/components/entrySignIn/entrySignIn.html b/Packages/active-entry/components/entrySignIn/entrySignIn.html index 4af315afb..b7cf8a26b 100755 --- a/Packages/active-entry/components/entrySignIn/entrySignIn.html +++ b/Packages/active-entry/components/entrySignIn/entrySignIn.html @@ -16,27 +16,47 @@
{{{getSignInMessage}}}
- + +