<?php
/**
* LDAP Auth Adapter
*
* @author Tom Maiaroto
* @website http://www.shift8creative.com
* @created 08-26-2010
*
* Lithium: the most rad php framework
*
* @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace app\extensions\adapter\auth;
use \lithium\core\Libraries;
/**
* The `Auth` adapter provides basic authentication facilities for checking credentials submitted
* via a web form against an LDAP server. To perform an authentication check, the adapter accepts
* an instance of a `Request` object which contains the submitted form data in its `$data` property.
*
* When a request is submitted, the adapter will take the form data from the `Request` object,
* apply any filters as appropriate (see the `'filters'` configuration setting below), and
* attempt to connect to an LDAP server using using the filtered data.
*
* If successfully connected, it will retrieve the user's account record and return it to be
* store in the session data.
*
* By default it's set to look for an LDAP server locally. You will probably need to in the least, adjust the basedn.
* You must always pass a "password" field in your request data. It will not be used to assemble the login RDN.
* You will pass in the `'fields'` key any field from the login form (request data) that need to be assembled into
* the login RDN. You can optionally pass additional parameters to be assembled in the RDN with the `'targetdn'` key.
*
* An example configuration might look like the following:
* {{{
* Auth::config(array(
* 'customer' => array(
* 'adapter' => 'Ldap',
* 'fields' => array('uid'),
* 'server' => array(
* 'host' => 'ldapserver.com',
* 'basedn' => 'o=ldapserver.com',
* 'targetdn' => 'ou=People'
* )
* )
* ));
* }}}
*
* Another example, if the "ou" field needed to be passed in the request data and be able to be changed on the form
* for some reason or another:
* {{{
* Auth::config(array(
* 'customer' => array(
* 'adapter' => 'Ldap',
* 'fields' => array('uid', 'ou'),
* 'server' => array(
* 'host' => 'ldapserver.com',
* 'basedn' => 'o=ldapserver.com'
* )
* )
* ));
* }}}
*
* To have the "ou" property be set to "People" you would maybe have a drop down with a name of "ou" on your login form.
* This might switch out the "ou" value for some reason. Perhaps you're selecting a "group" when logging in, etc.
*
* An example RDN that might be constructed would look like:
* uid=Myuser, ou=Person, o=ldapserver.com
*
* Then, again, the "password" field would be used with this RDN to login to the LDAP server.
*
* As mentioned, prior to any queries being executed, the request data is modified by any filters
* configured. Filters are callbacks which accept the value of a field as input, and return a
* modified version of the value as output. Filters can be any PHP callable, i.e. a closure or
* `array('ClassName', 'method')`. The Ldap auth adapter will filter the password field by default in order
* to hash it by `lithium\util\String::hash()`. You may or may not need to hash the password before sending to the
* LDAP server. Typically you won't be with an LDAP server, but any other fields you may need to alter for any reason,
* you can do so using callbacks that you define.
*
* Note that if you are specifying the `'fields'` configuration using key/value pairs, the key
* used to specify the filter must match the key side of the `'fields'` assignment.
*
* @see lithium\net\http\Request::$data
*/
class Ldap extends \lithium\core\Object {
/**
* The list of fields to extract from the `Request` object and use when querying the database.
* This can either be a simple array of field names, or a set of key/value pairs, which map
* the field names in the request to
*
* @var array
*/
protected $_fields = array();
/**
* Callback filters to apply to request data before using it in the authentication query. Each
* key in the array must match a request field specified in the `$_fields` property, and each
* value must either be a reference to a function or method name, or a closure. For example, to
* automatically hash passwords, ex.: `array('password' => array('\lithium\util\String', 'hash'))`.
*
* Optionally, you can specify a callback with no key, which will receive (and can modify) the
* entire credentials array before the query is executed, as in the following example:
* {{{
* Auth::config(array(
* 'members' => array(
* 'adapter' => 'Ldap',
* 'fields' => array('uid'),
* 'filters' => array(function($data) {
* // If the user is outside the company, then their account must have the
* // 'private' field set to true in order to log in:
* if (!preg_match('/@mycompany\.com$/', $data['email'])) {
* $data['private'] = true;
* }
* return $data;
* })
* )
* ));
* }}}
*
* @see app\extensions\auth\adapter\Ldap::$_fields
* @var array
*/
protected $_filters = array();
/**
* The settings used to connect to the LDAP server.
* You will define these in the configuration.
*
* @var array
*/
protected $_server = array();
/**
* List of configuration properties to automatically assign to the properties of the adapter
* when the class is constructed.
*
* @var array
*/
protected $_autoConfig = array('fields', 'server');
/**
* This will become the LDAP connection resource.
*
*/
protected $connection;
/**
* Sets the initial configuration for the `Ldap` adapter, as detailed below.
*
* @see app\extensions\auth\adapter\Ldap::$_server
* @see app\extensions\auth\adapter\Ldap::$_fields
* @see app\extensions\auth\adapter\Ldap::$_filters
* @param array $config Sets the configuration for the adapter, which has the following options:
* - `'server'` _array_: The LDAP server information required to connect including the basedn.
* - `'fields'` _array_: The fields to use for the login RDN.
* See the `$_fields` property for details.
* - `'filters'` _array_: Named callbacks to apply to request data before the user
* login request is made. See the `$_filters` property for more details.
*/
public function __construct(array $config = array()) {
$defaults = array(
'filters' => array(),
'fields' => array(
'uid'
),
'server' => array(
'basedn' => 'o=com',
'targetdn' => null,
'host' => '127.0.0.1',
'port' => 389,
'version' => 3,
'timeout' => 30
)
);
// Merge server defaults if specified in config.
if(isset($config['server'])) {
$config['server'] += $defaults['server'];
}
parent::__construct($config + $defaults);
}
/**
* Called by the `Auth` class to run an authentication check against an LDAP server using the
* credientials in a data container (a `Request` object), and returns an array of user
* information on success, or `false` on failure.
*
* @param object $credentials A data container which wraps the authentication credentials used
* to login to an LDAP server (usually a `Request` object). See the documentation
* for this class for further details.
* @param array $options Additional configuration options. The basedn or targetdn could be changed for example.
* @return array Returns an array containing user information on success, or `false` on failure.
*/
public function check($credentials, array $options = array()) {
$conditions = $this->_filters($credentials->data);
$options += $this->_config;
// Set the password (if no password was passed in the request data, we can try not using one...)
if(isset($conditions['password'])) {
$password = $conditions['password'];
} else {
$password = null;
}
// Then unset it, we don't want the password in the RDN, it's for the server login....
// IF for some reason we do need it in the RDN, then it has to be defined in the fields.
if(!in_array('password', $options['fields'])) {
unset($conditions['password']);
}
// Assemble the pieces for the RDN
$options['server']['targetdn'] = $this->_assembleDnFromArray($options['server']['targetdn']);
$options['server']['basedn'] = $this->_assembleDnFromArray($options['server']['basedn']);
/* This assembles all passed request data that is within the _fields property.
* It could include things like "uid" and maybe "ou" and such.
* It is a way to dynamically allow the user to change the DN that's used to login from the login form.
* Or maybe extra pieces for the DN are put into "targetdn" and the user is just presented with a form
* asking for a username and password, despite that there are other properties that will be used to make
* the complete RDN that's used to login.
*
* In the very least though, this will include some sort of username or uid.
*/
$rdn = $this->_assembleDnFromArray($conditions) . ', ';
// Again, a targetdn could be defined in the config to build part of the full RDN.
if(!empty($options['server']['targetdn'])) {
$rdn .= $options['server']['targetdn'] . ', ';
}
// The basedn is typically something simple and higher up in the tree.
if(!empty($options['server']['basedn'])) {
$rdn .= $options['server']['basedn'];
}
// Clean up any potential extra spaces or erroneous ending commas with the RDN
$rdn = trim($rdn);
if(substr($rdn, -1) == ',') {
$rdn = substr($rdn, 0, -1);
}
// Make the connection to the LDAP server
$this->connection = ldap_connect($options['server']['host'], $options['server']['port']);
// Set the version
ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, $options['server']['version']);
// Set the timeout
ldap_set_option($this->connection, LDAP_OPT_NETWORK_TIMEOUT, $options['server']['timeout']);
if(@ldap_bind($this->connection, $rdn, $password)) {
// var_dump('Authenticate success');
} else {
// var_dump('Failure');
return false;
}
// Get the user's record
$res = @ldap_read($this->connection, $rdn, '(objectclass=*)');
$user = $this->_ldapFormat(ldap_get_entries($this->connection, $res));
// Free up memory and close connection
ldap_free_result($this->connection);
ldap_unbind($this->connection);
return $user ? $user[0] : false;
}
/**
* Format the ldap results into a nicer looking array.
*
* @param $data Array[required] The array returned by ldap_get_entries()
* @return Array Formatted results in a much nicer key => value fashion.
*/
private function _ldapFormat($data=null) {
$res = array();
if(!$data) {
return $res;
}
foreach ($data as $key => $row){
if ($key === 'count')
continue;
foreach ($row as $key1 => $param){
if (!is_numeric($key1))
continue;
if ($row[$param]['count'] === 1)
$res[$key][$param] = $row[$param][0];
else {
foreach ($row[$param] as $key2 => $item) {
if ($key2 === 'count')
continue;
$res[$key][$param][] = $item;
}
}
$res[$key]['dn'] = $row['dn'];
}
}
return $res;
}
/**
* Assemble all/part of a DN from an array.
*
* @param $data Array[required] The key => value array of properties.
* @return String Formatted DN (ex. cn=user, dc=bar, dc=com)
*/
private function _assembleDnFromArray($data=null) {
if(!is_array($data)) {
return $data;
}
$length = count($data);
$string = '';
$i = 1;
foreach($data as $k => $v) {
$string .= $k . '=' . $v;
if($i < $length) {
$string .= ', ';
}
$i++;
}
return $string;
}
/**
* A pass-through method called by `Auth`. Returns the value of `$data`, which is written to
* a user's session. When implementing a custom adapter, this method may be used to modify or
* reject data before it is written to the session.
*
* @param array $data User data to be written to the session.
* @param array $options Adapter-specific options. Not implemented in the `Ldap` adapter.
* @return array Returns the value of `$data`.
*/
public function set($data, array $options = array()) {
return $data;
}
/**
* Called by `Auth` when a user session is terminated. Not implemented in the `Ldap` adapter.
*
* @param array $options Adapter-specific options. Not implemented in the `Ldap` adapter.
* @return void
*/
public function clear(array $options = array()) {
}
protected function _init() {
parent::_init();
if (isset($this->_fields[0])) {
$this->_fields = array_combine($this->_fields, $this->_fields);
}
}
/**
* Calls each registered callback, by field name.
*
* @param string $data Keyed form data.
* @return mixed Callback result.
*/
protected function _filters($data) {
$result = array();
// There should be a password passed always, it's not an optional/changeable field name.
// There may not be a callback for it, but if so, run it.
if(isset($data['password'])) {
$result['password'] = $data['password'];
if (isset($this->_filters['password'])) {
$result['password'] = call_user_func($this->_filters['password'], $result['password']);
}
}
// Run through the rest of the allowed fields and if any have callbacks, run those as well.
foreach ($this->_fields as $key => $field) {
$result[$field] = isset($data[$key]) ? $data[$key] : null;
if (isset($this->_filters[$key])) {
$result[$field] = call_user_func($this->_filters[$key], $result[$field]);
}
}
return isset($this->_filters[0]) ? call_user_func($this->_filters[0], $result) : $result;
}
}
?>