app\extensions\adapter\auth \ Ldap

LDAP Auth

Download |
| Newest | Edit
<?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;
	}
}
?>

Description

The LDAP Auth extension will authenticate against an LDAP server using provided credentials and server settings. Once successfully logged in, it will retrieve the user's record and return it to be stored in the session data.

Details

  • Version: 1
  • Created: 2010-08-26 02:46:36
  • File: app/extensions/adapter/auth/Ldap.php

Maintainers