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 }