'use strict';
var passport = require('passport-strategy');
var LdapAuth = require('ldapauth-fork');
var util = require('util');
/**
* Passport wrapper for ldapauth
*/
/**
* Options callback callback (ie. the callback given if giving a callback
* for options instead of an object)
*
* @callback optionsCallbackCallback
* @param {(Error|undefined)} err - Possible error
* @param {Object} options - Options object
*/
/**
* Options callback
*
* @callback optionsCallback
* @param {Object} req - HTTP request
* @param {optionsCallbackCallback} callback - The callback returning the options
*/
/**
* Verify done callback
*
* @callback verifyDoneCallback
* @param {(Error|undefined)} err - Possible error
* @param {(Object|boolean)} user - The verified user or false if not allowed
* @param {Object} [info] info - Additional info message
*/
/**
* Found LDAP user verify callback
*
* @callback verifyCallback
* @param {Object} user - The user object from LDAP
* @param {verifyDoneCallback} callback - The verify callback
*/
/**
* Found LDAP user verify callback with request
*
* @callback verifyReqCallback
* @param {Object} req - The HTTP request
* @param {Object} user - The user object from LDAP
* @param {verifyDoneCallback} callback - The verify callback
*/
/**
* @typedef credentialsLookupResult
* @type {object}
* @property {string} username - Username to use
* @property {string} password - Password to use
*/
/**
* @typedef credentialsLookupResultAlt
* @type {object}
* @property {string} user - Username to use
* @property {string} pass - Password to use
*/
/**
* Credentials lookup function
*
* @callback credentialsLookup
* @param {Object} req - The HTTP request
* @return {(credentialsLookupResult|credentialsLookupResultAlt)} - Found credentials
*/
/**
* Synchronous function for doing something with an error if handling
* errors as failures
*
* @callback failureErrorCallback
* @param {Error} err - The error occurred
*/
/**
* Add default values to options
*
* @private
* @param {Object} options - Options object
* @returns {Object} The given options with defaults filled
*/
var setDefaults = function(options) {
options.usernameField || (options.usernameField = 'username');
options.passwordField || (options.passwordField = 'password');
return options;
};
/**
* Strategy constructor
* <br>
*
* The LDAP authentication strategy authenticates requests based on the
* credentials submitted through an HTML-based login form.
* <br>
*
* Applications may supply a `verify` callback which accepts `user` object
* and then calls the `done` callback supplying a `user`, which should be set
* to `false` if user is not allowed to authenticate. If an exception occured,
* `err` should be set.
* <br>
*
* Options can be also given as function that accepts a callback end calls it
* with error and options arguments. Notice that the callback is executed on
* every authenticate call.
* <br>
*
* Example:
* <pre>
* var LdapStrategy = require('passport-ldapauth').Strategy;
* passport.use(new LdapStrategy({
* server: {
* url: 'ldap://localhost:389',
* bindDN: 'cn=root',
* bindCredentials: 'secret',
* searchBase: 'ou=passport-ldapauth',
* searchFilter: '(uid={{username}})',
* reconnect: true
* }
* },
* function(user, done) {
* return cb(null, user);
* }
* ));
* </pre>
* @constructor
* @param {(Object|optionsCallback)} options - Configuration options or options returning function
* @param {Object} options.server - [ldapauth-fork options]{@link https://github.com/vesse/node-ldapauth-fork}
* @param {string} [options.usernameField=username] - Form field name for username
* @param {string} [options.passwordField=password] - Form field name for password
* @param {boolean} [options.passReqToCallback=false] - If true, request is passed to verify callback
* @param {credentialsLookup} [options.credentialsLookup] - Credentials lookup function to use instead of default
* @param {boolean} [options.handleErrorAsFailures=false] - Set to true to handle errors as login failures
* @param {failureErrorCallback} [options.failureErrorCallback] - Function receives the occurred error when errors handled as failures
* @param {(verifyCallback|verifyReqCallback|undefined)} [verify] - User verify callback
*/
var Strategy = function(options, verify) {
// We now accept function as options as well so we cannot assume anymore
// that a call with a function parameter only would have skipped options
// and just provided a verify callback
if (!options) {
throw new Error('LDAP authentication strategy requires options');
}
this.options = null;
this.getOptions = null;
if (typeof options === 'object') {
this.options = setDefaults(options);
} else if (typeof options === 'function') {
this.getOptions = options;
}
passport.Strategy.call(this);
this.name = 'ldapauth';
this.verify = verify;
};
util.inherits(Strategy, passport.Strategy);
/* eslint-disable */
/**
* Get value for given field from given object. Taken from passport-local,
* copyright 2011-2013 Jared Hanson
*
* @private
* @param {Object} obj - The HTTP request object
* @param {string} field - The field name to look for
* @returns {string|null} - Found value for the field or null
*/
var lookup = function(obj, field) {
var i, len, chain, prop;
if (!obj) { return null; }
chain = field.split(']').join('').split('[');
for (i = 0, len = chain.length; i < len; i++) {
prop = obj[chain[i]];
if (typeof(prop) === 'undefined') { return null; }
if (typeof(prop) !== 'object') { return prop; }
obj = prop;
}
return null;
};
/* eslint-enable */
/**
* Verify the outcome of caller verify function - even if authentication (and
* usually authorization) is taken care by LDAP there may be reasons why
* a verify callback is provided, and again reasons why it may reject login
* for a valid user.
*
* @private
* @returns {undefined}
*/
var verify = function() {
// Callback given to user given verify function.
return function(err, user, info) {
if (err) {
return this.error(err);
}
if (!user) {
return this.fail(info);
}
return this.success(user, info);
}.bind(this);
};
/**
* Execute failureErrorCallback if provided
*
* @private
* @param {Error} err - The error occurred
* @returns {undefined}
*/
var handleErrorAsFailure = function(err) {
if (typeof this.options.failureErrorCallback === 'function') {
this.options.failureErrorCallback(err);
}
return this.fail(err, 500);
};
/**
* The actual authenticate implementation
*
* @private
* @param {Object} req - The HTTP request
* @param {Object} [options] - Flash messages
* @returns {undefined}
*/
var handleAuthentication = function(req, options) {
var username;
var password;
var ldap;
var errorHandler;
options || (options = {}); // eslint-disable-line no-param-reassign
if (typeof this.options.credentialsLookup === 'function') {
var credentials = this.options.credentialsLookup(req);
if (credentials != null) {
// name and pass as a courtesy for those who use basic-auth directly as
// they're likely the main user group.
username = credentials.username || credentials.name;
password = credentials.password || credentials.pass;
}
} else {
username = lookup(req.body, this.options.usernameField) || lookup(req.query, this.options.usernameField);
password = lookup(req.body, this.options.passwordField) || lookup(req.query, this.options.passwordField);
}
if (!username || !password) {
return this.fail({ message: options.badRequestMessage || 'Missing credentials' }, 400);
}
errorHandler = this.options.handleErrorsAsFailures === true ? handleErrorAsFailure.bind(this) : this.error.bind(this);
/**
* AD possible messages
* http://www-01.ibm.com/support/docview.wss?uid=swg21290631
*/
var messages = {
'530': options.invalidLogonHours || 'Not Permitted to login at this time',
'531': options.invalidWorkstation || 'Not permited to logon at this workstation',
'532': options.passwordExpired || 'Password expired',
'533': options.accountDisabled || 'Account disabled',
'534': options.accountDisabled || 'Account disabled',
'701': options.accountExpired || 'Account expired',
'773': options.passwordMustChange || 'User must reset password',
'775': options.accountLockedOut || 'User account locked',
default: options.invalidCredentials || 'Invalid username/password'
};
ldap = new LdapAuth(this.options.server);
ldap.on('error', errorHandler);
ldap.authenticate(username, password, function(err, user) {
ldap.close(function(){
// We don't care about the closing
});
if (err) {
// Invalid credentials / user not found are not errors but login failures
if (err.name === 'InvalidCredentialsError' || err.name === 'NoSuchObjectError' || (typeof err === 'string' && err.match(/no such user/i))) {
var message = options.invalidCredentials || 'Invalid username/password';
if (err.message) {
var ldapComment = err.message.match(/data ([0-9a-fA-F]*), v[0-9a-fA-F]*/);
if (ldapComment && ldapComment[1]){
message = messages[ldapComment[1]] || messages['default'];
}
}
return this.fail({ message: message }, 401);
}
if (err.name === 'ConstraintViolationError'){
return this.fail({ message: options.constraintViolation || 'Exceeded password retry limit, account locked' }, 401);
}
// Other errors are (most likely) real errors
return errorHandler(err);
}
if (!user) {
return this.fail({ message: options.userNotFound || 'Invalid username/password' }, 401);
}
// Execute given verify function
if (this.verify) {
if (this.options.passReqToCallback) {
return this.verify(req, user, verify.call(this));
} else {
return this.verify(user, verify.call(this));
}
} else {
return this.success(user);
}
}.bind(this));
};
/**
* Authenticate the request coming from a form or such.
*
* @param {Object} req - The HTTP request
* @param {Object} [options] - Authentication options (flash messages). All messages have default values.
* @param {string} [options.badRequestMessage] - Message for missing username/password
* @param {string} [options.invalidCredentials] - Message for InvalidCredentialsError, NoSuchObjectError, and /no such user/ LDAP errors
* @param {string} [options.userNotFound] - Message for user not found
* @param {string} [options.constraintViolation] - Message when account is locked (or other constraint violation)
* @param {string} [options.invalidLogonHours] - Message for Windows AD invalidLogonHours error
* @param {string} [options.invalidWorkstation] - Message for Windows AD invalidWorkstation error
* @param {string} [options.passwordExpired] - Message for Windows AD passwordExpired error
* @param {string} [options.accountDisabled] - Message for Windows AD accountDisabled error
* @param {string} [options.accountExpired] - Message for Windows AD accountExpired error
* @param {string} [options.passwordMustChange] - Message for Windows AD passwordMustChange error
* @param {string} [options.accountLockedOut] - Message for Windows AD accountLockedOut error
* @returns {undefined}
*/
Strategy.prototype.authenticate = function(req, options) {
if ((typeof this.options === 'object') && (!this.getOptions)) {
return handleAuthentication.call(this, req, options);
}
var callback = function(err, configuration) {
if (err) {
return this.fail(err);
}
this.options = setDefaults(configuration);
handleAuthentication.call(this, req, options);
};
// Added functionality: getOptions can accept now up to 2 parameters
if (this.getOptions.length === 1) { // Accepts 1 parameter, backwards compatibility
this.getOptions(callback.bind(this));
} else { // Accepts 2 parameters, pass request as well
this.getOptions(req, callback.bind(this));
}
};
module.exports = Strategy;