File indexing completed on 2024-12-22 05:36:28

0001 <?php
0002 /**
0003  * Zend Framework
0004  *
0005  * LICENSE
0006  *
0007  * This source file is subject to the new BSD license that is bundled
0008  * with this package in the file LICENSE.txt.
0009  * It is also available through the world-wide-web at this URL:
0010  * http://framework.zend.com/license/new-bsd
0011  * If you did not receive a copy of the license and are unable to
0012  * obtain it through the world-wide-web, please send an email
0013  * to license@zend.com so we can send you a copy immediately.
0014  *
0015  * @category   Zend
0016  * @package    Zend_Auth
0017  * @subpackage Zend_Auth_Adapter
0018  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0019  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0020  * @version    $Id$
0021  */
0022 
0023 /**
0024  * @see Zend_Auth_Adapter_Interface
0025  */
0026 // require_once 'Zend/Auth/Adapter/Interface.php';
0027 
0028 /**
0029  * @category   Zend
0030  * @package    Zend_Auth
0031  * @subpackage Zend_Auth_Adapter
0032  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0033  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0034  */
0035 class Zend_Auth_Adapter_Ldap implements Zend_Auth_Adapter_Interface
0036 {
0037 
0038     /**
0039      * The Zend_Ldap context.
0040      *
0041      * @var Zend_Ldap
0042      */
0043     protected $_ldap = null;
0044 
0045     /**
0046      * The array of arrays of Zend_Ldap options passed to the constructor.
0047      *
0048      * @var array
0049      */
0050     protected $_options = null;
0051 
0052     /**
0053      * The username of the account being authenticated.
0054      *
0055      * @var string
0056      */
0057     protected $_username = null;
0058 
0059     /**
0060      * The password of the account being authenticated.
0061      *
0062      * @var string
0063      */
0064     protected $_password = null;
0065 
0066     /**
0067      * The DN of the authenticated account. Used to retrieve the account entry on request.
0068      *
0069      * @var string
0070      */
0071     protected $_authenticatedDn = null;
0072 
0073     /**
0074      * Constructor
0075      *
0076      * @param  array  $options  An array of arrays of Zend_Ldap options
0077      * @param  string $username The username of the account being authenticated
0078      * @param  string $password The password of the account being authenticated
0079      */
0080     public function __construct(array $options = array(), $username = null, $password = null)
0081     {
0082         $this->setOptions($options);
0083         if ($username !== null) {
0084             $this->setUsername($username);
0085         }
0086         if ($password !== null) {
0087             $this->setPassword($password);
0088         }
0089     }
0090 
0091     /**
0092      * Returns the array of arrays of Zend_Ldap options of this adapter.
0093      *
0094      * @return array|null
0095      */
0096     public function getOptions()
0097     {
0098         return $this->_options;
0099     }
0100 
0101     /**
0102      * Sets the array of arrays of Zend_Ldap options to be used by
0103      * this adapter.
0104      *
0105      * @param  array $options The array of arrays of Zend_Ldap options
0106      * @return Zend_Auth_Adapter_Ldap Provides a fluent interface
0107      */
0108     public function setOptions($options)
0109     {
0110         $this->_options = is_array($options) ? $options : array();
0111         return $this;
0112     }
0113 
0114     /**
0115      * Returns the username of the account being authenticated, or
0116      * NULL if none is set.
0117      *
0118      * @return string|null
0119      */
0120     public function getUsername()
0121     {
0122         return $this->_username;
0123     }
0124 
0125     /**
0126      * Sets the username for binding
0127      *
0128      * @param  string $username The username for binding
0129      * @return Zend_Auth_Adapter_Ldap Provides a fluent interface
0130      */
0131     public function setUsername($username)
0132     {
0133         $this->_username = (string) $username;
0134         return $this;
0135     }
0136 
0137     /**
0138      * Returns the password of the account being authenticated, or
0139      * NULL if none is set.
0140      *
0141      * @return string|null
0142      */
0143     public function getPassword()
0144     {
0145         return $this->_password;
0146     }
0147 
0148     /**
0149      * Sets the passwort for the account
0150      *
0151      * @param  string $password The password of the account being authenticated
0152      * @return Zend_Auth_Adapter_Ldap Provides a fluent interface
0153      */
0154     public function setPassword($password)
0155     {
0156         $this->_password = (string) $password;
0157         return $this;
0158     }
0159 
0160     /**
0161      * setIdentity() - set the identity (username) to be used
0162      *
0163      * Proxies to {@see setUsername()}
0164      *
0165      * Closes ZF-6813
0166      *
0167      * @param  string $identity
0168      * @return Zend_Auth_Adapter_Ldap Provides a fluent interface
0169      */
0170     public function setIdentity($identity)
0171     {
0172         return $this->setUsername($identity);
0173     }
0174 
0175     /**
0176      * setCredential() - set the credential (password) value to be used
0177      *
0178      * Proxies to {@see setPassword()}
0179      *
0180      * Closes ZF-6813
0181      *
0182      * @param  string $credential
0183      * @return Zend_Auth_Adapter_Ldap Provides a fluent interface
0184      */
0185     public function setCredential($credential)
0186     {
0187         return $this->setPassword($credential);
0188     }
0189 
0190     /**
0191      * Returns the LDAP Object
0192      *
0193      * @return Zend_Ldap The Zend_Ldap object used to authenticate the credentials
0194      */
0195     public function getLdap()
0196     {
0197         if ($this->_ldap === null) {
0198             /**
0199              * @see Zend_Ldap
0200              */
0201             // require_once 'Zend/Ldap.php';
0202             $this->_ldap = new Zend_Ldap();
0203         }
0204 
0205         return $this->_ldap;
0206     }
0207 
0208     /**
0209      * Set an Ldap connection
0210      *
0211      * @param Zend_Ldap $ldap An existing Ldap object
0212      * @return Zend_Auth_Adapter_Ldap Provides a fluent interface
0213      */
0214     public function setLdap(Zend_Ldap $ldap)
0215     {
0216         $this->_ldap = $ldap;
0217 
0218         $this->setOptions(array($ldap->getOptions()));
0219 
0220         return $this;
0221     }
0222 
0223     /**
0224      * Returns a domain name for the current LDAP options. This is used
0225      * for skipping redundant operations (e.g. authentications).
0226      *
0227      * @return string
0228      */
0229     protected function _getAuthorityName()
0230     {
0231         $options = $this->getLdap()->getOptions();
0232         $name = $options['accountDomainName'];
0233         if (!$name)
0234             $name = $options['accountDomainNameShort'];
0235         return $name ? $name : '';
0236     }
0237 
0238     /**
0239      * Authenticate the user
0240      *
0241      * @throws Zend_Auth_Adapter_Exception
0242      * @return Zend_Auth_Result
0243      */
0244     public function authenticate()
0245     {
0246         /**
0247          * @see Zend_Ldap_Exception
0248          */
0249         // require_once 'Zend/Ldap/Exception.php';
0250 
0251         $messages = array();
0252         $messages[0] = ''; // reserved
0253         $messages[1] = ''; // reserved
0254 
0255         $username = $this->_username;
0256         $password = $this->_password;
0257 
0258         if (!$username) {
0259             $code = Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND;
0260             $messages[0] = 'A username is required';
0261             return new Zend_Auth_Result($code, '', $messages);
0262         }
0263         if (!$password) {
0264             /* A password is required because some servers will
0265              * treat an empty password as an anonymous bind.
0266              */
0267             $code = Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID;
0268             $messages[0] = 'A password is required';
0269             return new Zend_Auth_Result($code, '', $messages);
0270         }
0271 
0272         $ldap = $this->getLdap();
0273 
0274         $code = Zend_Auth_Result::FAILURE;
0275         $messages[0] = "Authority not found: $username";
0276         $failedAuthorities = array();
0277 
0278         /* Iterate through each server and try to authenticate the supplied
0279          * credentials against it.
0280          */
0281         foreach ($this->_options as $name => $options) {
0282 
0283             if (!is_array($options)) {
0284                 /**
0285                  * @see Zend_Auth_Adapter_Exception
0286                  */
0287                 // require_once 'Zend/Auth/Adapter/Exception.php';
0288                 throw new Zend_Auth_Adapter_Exception('Adapter options array not an array');
0289             }
0290             $adapterOptions = $this->_prepareOptions($ldap, $options);
0291             $dname = '';
0292 
0293             try {
0294                 if ($messages[1])
0295                     $messages[] = $messages[1];
0296                 $messages[1] = '';
0297                 $messages[] = $this->_optionsToString($options);
0298 
0299                 $dname = $this->_getAuthorityName();
0300                 if (isset($failedAuthorities[$dname])) {
0301                     /* If multiple sets of server options for the same domain
0302                      * are supplied, we want to skip redundant authentications
0303                      * where the identity or credentials where found to be
0304                      * invalid with another server for the same domain. The
0305                      * $failedAuthorities array tracks this condition (and also
0306                      * serves to supply the original error message).
0307                      * This fixes issue ZF-4093.
0308                      */
0309                     $messages[1] = $failedAuthorities[$dname];
0310                     $messages[] = "Skipping previously failed authority: $dname";
0311                     continue;
0312                 }
0313 
0314                 $canonicalName = $ldap->getCanonicalAccountName($username);
0315                 $ldap->bind($canonicalName, $password);
0316                 /*
0317                  * Fixes problem when authenticated user is not allowed to retrieve
0318                  * group-membership information or own account.
0319                  * This requires that the user specified with "username" and optionally
0320                  * "password" in the Zend_Ldap options is able to retrieve the required
0321                  * information.
0322                  */
0323                 $requireRebind = false;
0324                 if (isset($options['username'])) {
0325                     $ldap->bind();
0326                     $requireRebind = true;
0327                 }
0328                 $dn = $ldap->getCanonicalAccountName($canonicalName, Zend_Ldap::ACCTNAME_FORM_DN);
0329 
0330                 $groupResult = $this->_checkGroupMembership($ldap, $canonicalName, $dn, $adapterOptions);
0331                 if ($groupResult === true) {
0332                     $this->_authenticatedDn = $dn;
0333                     $messages[0] = '';
0334                     $messages[1] = '';
0335                     $messages[] = "$canonicalName authentication successful";
0336                     if ($requireRebind === true) {
0337                         // rebinding with authenticated user
0338                         $ldap->bind($dn, $password);
0339                     }
0340                     return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $canonicalName, $messages);
0341                 } else {
0342                     $messages[0] = 'Account is not a member of the specified group';
0343                     $messages[1] = $groupResult;
0344                     $failedAuthorities[$dname] = $groupResult;
0345                 }
0346             } catch (Zend_Ldap_Exception $zle) {
0347 
0348                 /* LDAP based authentication is notoriously difficult to diagnose. Therefore
0349                  * we bend over backwards to capture and record every possible bit of
0350                  * information when something goes wrong.
0351                  */
0352 
0353                 $err = $zle->getCode();
0354 
0355                 if ($err == Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH) {
0356                     /* This error indicates that the domain supplied in the
0357                      * username did not match the domains in the server options
0358                      * and therefore we should just skip to the next set of
0359                      * server options.
0360                      */
0361                     continue;
0362                 } else if ($err == Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT) {
0363                     $code = Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND;
0364                     $messages[0] = "Account not found: $username";
0365                     $failedAuthorities[$dname] = $zle->getMessage();
0366                 } else if ($err == Zend_Ldap_Exception::LDAP_INVALID_CREDENTIALS) {
0367                     $code = Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID;
0368                     $messages[0] = 'Invalid credentials';
0369                     $failedAuthorities[$dname] = $zle->getMessage();
0370                 } else {
0371                     $line = $zle->getLine();
0372                     $messages[] = $zle->getFile() . "($line): " . $zle->getMessage();
0373                     $messages[] = preg_replace(
0374             '/\b'.preg_quote(substr($password, 0, 15), '/').'\b/',
0375             '*****',
0376             $zle->getTraceAsString()
0377           );
0378                     $messages[0] = 'An unexpected failure occurred';
0379                 }
0380                 $messages[1] = $zle->getMessage();
0381             }
0382         }
0383 
0384         $msg = isset($messages[1]) ? $messages[1] : $messages[0];
0385         $messages[] = "$username authentication failed: $msg";
0386 
0387         return new Zend_Auth_Result($code, $username, $messages);
0388     }
0389 
0390     /**
0391      * Sets the LDAP specific options on the Zend_Ldap instance
0392      *
0393      * @param  Zend_Ldap $ldap
0394      * @param  array $options
0395      * @return array of auth-adapter specific options
0396      */
0397     protected function _prepareOptions(Zend_Ldap $ldap, array $options)
0398     {
0399         $adapterOptions = array(
0400             'group'       => null,
0401             'groupDn'     => $ldap->getBaseDn(),
0402             'groupScope'  => Zend_Ldap::SEARCH_SCOPE_SUB,
0403             'groupAttr'   => 'cn',
0404             'groupFilter' => 'objectClass=groupOfUniqueNames',
0405             'memberAttr'  => 'uniqueMember',
0406             'memberIsDn'  => true
0407         );
0408         foreach ($adapterOptions as $key => $value) {
0409             if (array_key_exists($key, $options)) {
0410                 $value = $options[$key];
0411                 unset($options[$key]);
0412                 switch ($key) {
0413                     case 'groupScope':
0414                         $value = (int)$value;
0415                         if (in_array($value, array(Zend_Ldap::SEARCH_SCOPE_BASE,
0416                                 Zend_Ldap::SEARCH_SCOPE_ONE, Zend_Ldap::SEARCH_SCOPE_SUB), true)) {
0417                            $adapterOptions[$key] = $value;
0418                         }
0419                         break;
0420                     case 'memberIsDn':
0421                         $adapterOptions[$key] = ($value === true ||
0422                                 $value === '1' || strcasecmp($value, 'true') == 0);
0423                         break;
0424                     default:
0425                         $adapterOptions[$key] = trim($value);
0426                         break;
0427                 }
0428             }
0429         }
0430         $ldap->setOptions($options);
0431         return $adapterOptions;
0432     }
0433 
0434     /**
0435      * Checks the group membership of the bound user
0436      *
0437      * @param  Zend_Ldap $ldap
0438      * @param  string    $canonicalName
0439      * @param  string    $dn
0440      * @param  array     $adapterOptions
0441      * @return string|true
0442      */
0443     protected function _checkGroupMembership(Zend_Ldap $ldap, $canonicalName, $dn, array $adapterOptions)
0444     {
0445         if ($adapterOptions['group'] === null) {
0446             return true;
0447         }
0448 
0449         if ($adapterOptions['memberIsDn'] === false) {
0450             $user = $canonicalName;
0451         } else {
0452             $user = $dn;
0453         }
0454 
0455         /**
0456          * @see Zend_Ldap_Filter
0457          */
0458         // require_once 'Zend/Ldap/Filter.php';
0459         $groupName = Zend_Ldap_Filter::equals($adapterOptions['groupAttr'], $adapterOptions['group']);
0460         $membership = Zend_Ldap_Filter::equals($adapterOptions['memberAttr'], $user);
0461         $group = Zend_Ldap_Filter::andFilter($groupName, $membership);
0462         $groupFilter = $adapterOptions['groupFilter'];
0463         if (!empty($groupFilter)) {
0464             $group = $group->addAnd($groupFilter);
0465         }
0466 
0467         $result = $ldap->count($group, $adapterOptions['groupDn'], $adapterOptions['groupScope']);
0468 
0469         if ($result === 1) {
0470             return true;
0471         } else {
0472             return 'Failed to verify group membership with ' . $group->toString();
0473         }
0474     }
0475 
0476     /**
0477      * getAccountObject() - Returns the result entry as a stdClass object
0478      *
0479      * This resembles the feature {@see Zend_Auth_Adapter_DbTable::getResultRowObject()}.
0480      * Closes ZF-6813
0481      *
0482      * @param  array $returnAttribs
0483      * @param  array $omitAttribs
0484      * @return stdClass|boolean
0485      */
0486     public function getAccountObject(array $returnAttribs = array(), array $omitAttribs = array())
0487     {
0488         if (!$this->_authenticatedDn) {
0489             return false;
0490         }
0491 
0492         $returnObject = new stdClass();
0493 
0494         $returnAttribs = array_map('strtolower', $returnAttribs);
0495         $omitAttribs   = array_map('strtolower', $omitAttribs);
0496         $returnAttribs = array_diff($returnAttribs, $omitAttribs);
0497 
0498         $entry = $this->getLdap()->getEntry($this->_authenticatedDn, $returnAttribs, true);
0499         foreach ($entry as $attr => $value) {
0500             if (in_array($attr, $omitAttribs)) {
0501                 // skip attributes marked to be omitted
0502                 continue;
0503             }
0504             if (is_array($value)) {
0505                 $returnObject->$attr = (count($value) > 1) ? $value : $value[0];
0506             } else {
0507                 $returnObject->$attr = $value;
0508             }
0509         }
0510         return $returnObject;
0511     }
0512 
0513     /**
0514      * Converts options to string
0515      *
0516      * @param  array $options
0517      * @return string
0518      */
0519     private function _optionsToString(array $options)
0520     {
0521         $str = '';
0522         foreach ($options as $key => $val) {
0523             if ($key === 'password')
0524                 $val = '*****';
0525             if ($str)
0526                 $str .= ',';
0527             $str .= $key . '=' . $val;
0528         }
0529         return $str;
0530     }
0531 }