366 lines
13 KiB
JavaScript
366 lines
13 KiB
JavaScript
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
|
|
};
|
|
}
|
|
|
|
}); |