* $credentials = new ezcAuthenticationPasswordCredentials( 'jan.modaal', 'qwerty' );
* $ldap = new ezcAuthenticationLdapInfo( 'localhost', 'uid=%id%', 'dc=example,dc=com', 389 );
* $authentication = new ezcAuthentication( $credentials );
* $authentication->addFilter( new ezcAuthenticationLdapFilter( $ldap ) );
* // add more filters if needed
* if ( !$authentication->run() )
* {
* // authentication did not succeed, so inform the user
* $status = $authentication->getStatus();
* $err = array(
* 'ezcAuthenticationLdapFilter' => array(
* ezcAuthenticationLdapFilter::STATUS_USERNAME_INCORRECT => 'Incorrect username',
* ezcAuthenticationLdapFilter::STATUS_PASSWORD_INCORRECT => 'Incorrect password'
* )
* );
* foreach ( $status as $line )
* {
* list( $key, $value ) = each( $line );
* echo $err[$key][$value] . "\n";
* }
* }
* else
* {
* // authentication succeeded, so allow the user to see his content
* }
*
*
* Extra data can be fetched from the LDAP server during the authentication
* process, by registering the data to be fetched before calling run(). Example:
*
* // $filter is an ezcAuthenticationLdapFilter object
* $filter->registerFetchData( array( 'name', 'company', 'mobile' ) );
*
* // after run()
* $data = $filter->fetchData();
*
*
* The $data array will be something like:
*
* array( 'name' = > array( 'Dr. No' ),
* 'company' => array( 'SPECTRE' ),
* 'mobile' => array( '555-7732873' )
* );
*
*
* @property ezcAuthenticationLdapInfo $ldap
* Structure which holds the LDAP server hostname, entry format and base,
* and port.
*
* @package Authentication
* @version 1.2
* @mainclass
*/
class ezcAuthenticationLdapFilter extends ezcAuthenticationFilter implements ezcAuthenticationDataFetch
{
/**
* Username is not found in the database.
*/
const STATUS_USERNAME_INCORRECT = 1;
/**
* Password is incorrect.
*/
const STATUS_PASSWORD_INCORRECT = 2;
/**
* Use plain-text password and no encryption for the connection (default).
*/
const PROTOCOL_PLAIN = 1;
/**
* Use plain-text password and TLS connection.
*/
const PROTOCOL_TLS = 2;
/**
* Holds the attributes which will be requested during the authentication
* process.
*
* Usually it has this structure:
*
* array( 'name', 'company', 'mobile' );
*
*
* @var array(string)
*/
protected $requestedData = array();
/**
* Holds the extra data fetched during the authentication process.
*
* Usually it has this structure:
*
* array( 'name' = > array( 'Dr. No' ),
* 'company' => array( 'SPECTRE' ),
* 'mobile' => array( '555-7732873' )
* );
*
*
* @var array(string=>mixed)
*/
protected $data = array();
/**
* Holds the properties of this class.
*
* @var array(string=>mixed)
*/
private $properties = array();
/**
* Creates a new object of this class.
*
* @throws ezcBaseExtensionNotFoundException
* if the PHP ldap extension is not installed
* @param ezcAuthenticationLdapInfo $ldap How to connect to LDAP
* @param ezcAuthenticationLdapOptions $options Options for this class
*/
public function __construct( ezcAuthenticationLdapInfo $ldap, ezcAuthenticationLdapOptions $options = null )
{
if ( !ezcBaseFeatures::hasExtensionSupport( 'ldap' ) )
{
throw new ezcBaseExtensionNotFoundException( 'ldap', null, "PHP not configured with --with-ldap." );
}
$this->ldap = $ldap;
$this->options = ( $options === null ) ? new ezcAuthenticationLdapOptions() : $options;
}
/**
* Sets the property $name to $value.
*
* @throws ezcBasePropertyNotFoundException
* if the property $name does not exist
* @throws ezcBaseValueException
* if $value is not correct for the property $name
* @param string $name The name of the property to set
* @param mixed $value The new value of the property
* @ignore
*/
public function __set( $name, $value )
{
switch ( $name )
{
case 'ldap':
if ( $value instanceof ezcAuthenticationLdapInfo )
{
$this->properties[$name] = $value;
}
else
{
throw new ezcBaseValueException( $name, $value, 'ezcAuthenticationLdapInfo' );
}
break;
default:
throw new ezcBasePropertyNotFoundException( $name );
}
}
/**
* Returns the value of the property $name.
*
* @throws ezcBasePropertyNotFoundException
* if the property $name does not exist
* @param string $name The name of the property for which to return the value
* @return mixed
* @ignore
*/
public function __get( $name )
{
switch ( $name )
{
case 'ldap':
return $this->properties[$name];
default:
throw new ezcBasePropertyNotFoundException( $name );
}
}
/**
* Returns true if the property $name is set, otherwise false.
*
* @param string $name The name of the property to test if it is set
* @return bool
* @ignore
*/
public function __isset( $name )
{
switch ( $name )
{
case 'ldap':
return isset( $this->properties[$name] );
default:
return false;
}
}
/**
* Runs the filter and returns a status code when finished.
*
* @throws ezcAuthenticationLdapException
* if the connecting and binding to the LDAP server could not be performed
* @param ezcAuthenticationPasswordCredentials $credentials Authentication credentials
* @return int
*/
public function run( $credentials )
{
$protocol = 'ldap://'; // 'ldaps://' will be implemented later (if ever, as TLS is better)
// null, false or empty string passwords are not accepted, as on some servers
// they could cause the LDAP binding to succeed
if ( empty( $credentials->password ) )
{
return self::STATUS_PASSWORD_INCORRECT;
}
$connection = $this->ldapConnect( $this->ldap->host, $this->ldap->port );
if ( !$connection )
{
// OpenLDAP 2.x.x will not throw an exception because $connection is always a resource
throw new ezcAuthenticationLdapException( "Could not connect to host '{$protocol}{$this->ldap->host}:{$this->ldap->port}'." );
}
// without using version 3, TLS and other stuff won't work
ldap_set_option( $connection, LDAP_OPT_PROTOCOL_VERSION, 3 );
ldap_set_option( $connection, LDAP_OPT_REFERRALS, 0 );
// try to use a TLS connection
if ( $this->options->protocol === self::PROTOCOL_TLS || $this->ldap->protocol === self::PROTOCOL_TLS )
{
if ( $this->ldapStartTls( $connection ) )
{
// using TLS, so continue
}
else
{
throw new ezcAuthenticationLdapException( "Could not connect to host '{$protocol}{$this->ldap->host}:{$this->ldap->port}'." );
}
}
// bind anonymously to see if username exists in the directory
if ( @ldap_bind( $connection ) )
{
$search = ldap_search( $connection, $this->ldap->base, str_replace( '%id%', $credentials->id, $this->ldap->format ), $this->requestedData );
if ( ldap_count_entries( $connection, $search ) === 0 )
{
ldap_close( $connection );
return self::STATUS_USERNAME_INCORRECT;
}
// username exists, so check if we can bind with the provided password
if ( @ldap_bind( $connection, str_replace( '%id%', $credentials->id, $this->ldap->format ) . ',' . $this->ldap->base, $credentials->password ) )
{
// retrieve extra authentication data
if ( count( $this->requestedData ) > 0 )
{
$entry = ldap_first_entry( $connection, $search );
$attributes = ldap_get_attributes( $connection, $entry );
foreach ( $this->requestedData as $attribute )
{
// ignore case of $attribute
if ( isset( $attributes[$attribute] )
|| isset( $attributes[strtolower( $attribute )] ) )
{
for ( $i = 0; $i < $attributes[$attribute]['count']; $i++ )
{
$this->data[$attribute][] = $attributes[$attribute][$i];
}
}
}
}
ldap_close( $connection );
return self::STATUS_OK;
}
}
// bind failed, so something must be wrong (connection error or password incorrect)
$err = ldap_errno( $connection );
ldap_close( $connection );
// error codes: 0 = success, 49 = invalidCredentials, 50 = insufficientAccessRights
// so if any other codes appear it must mean that we could not connect to
// the LDAP host
if ( $err !== 0 && $err !== 49 && $err !== 50 )
{
throw new ezcAuthenticationLdapException( "Could not connect to host '{$protocol}{$this->ldap->host}:{$this->ldap->port}'", $err, ldap_err2str( $err ) );
}
return self::STATUS_PASSWORD_INCORRECT;
}
/**
* Wraps around the ldap_connect() function.
*
* Returns the connection as a resource if it was successful.
*
* @param string $host The LDAP hostname
* @param int $port The LDAP port to connect to $host, default 389
* @return mixed
*/
protected function ldapConnect( $host, $port = 389 )
{
return ldap_connect( $host, $port );
}
/**
* Wraps around the ldap_start_tls() function.
*
* Returns true if it was possible to start a TLS connection on the provided
* $connection.
*
* @param mixed $connection An established LDAP connection
* @return bool
*/
protected function ldapStartTls( $connection )
{
return @ldap_start_tls( $connection );
}
/**
* Registers which extra data to fetch during the authentication process.
*
* The input $data is an array of attributes to request, for example:
*
* array( 'name', 'company', 'mobile' );
*
*
* @param array(string) $data A list of attributes to fetch during authentication
*/
public function registerFetchData( array $data = array() )
{
$this->requestedData = $data;
}
/**
* Returns the extra data fetched during the authentication process.
*
* The return is something like:
*
* array( 'name' = > array( 'Dr. No' ),
* 'company' => array( 'SPECTRE' ),
* 'mobile' => array( '555-7732873' )
* );
*
*
* @return array(string=>mixed)
*/
public function fetchData()
{
return $this->data;
}
}
?>