|
1 <?php |
|
2 /** |
|
3 * Zend Framework |
|
4 * |
|
5 * LICENSE |
|
6 * |
|
7 * This source file is subject to the new BSD license that is bundled |
|
8 * with this package in the file LICENSE.txt. |
|
9 * It is also available through the world-wide-web at this URL: |
|
10 * http://framework.zend.com/license/new-bsd |
|
11 * If you did not receive a copy of the license and are unable to |
|
12 * obtain it through the world-wide-web, please send an email |
|
13 * to license@zend.com so we can send you a copy immediately. |
|
14 * |
|
15 * @category Zend |
|
16 * @package Zend_Auth |
|
17 * @subpackage Zend_Auth_Adapter |
|
18 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
19 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
20 * @version $Id: Ldap.php 21319 2010-03-04 16:02:16Z sgehrig $ |
|
21 */ |
|
22 |
|
23 /** |
|
24 * @see Zend_Auth_Adapter_Interface |
|
25 */ |
|
26 require_once 'Zend/Auth/Adapter/Interface.php'; |
|
27 |
|
28 /** |
|
29 * @category Zend |
|
30 * @package Zend_Auth |
|
31 * @subpackage Zend_Auth_Adapter |
|
32 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
33 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
34 */ |
|
35 class Zend_Auth_Adapter_Ldap implements Zend_Auth_Adapter_Interface |
|
36 { |
|
37 |
|
38 /** |
|
39 * The Zend_Ldap context. |
|
40 * |
|
41 * @var Zend_Ldap |
|
42 */ |
|
43 protected $_ldap = null; |
|
44 |
|
45 /** |
|
46 * The array of arrays of Zend_Ldap options passed to the constructor. |
|
47 * |
|
48 * @var array |
|
49 */ |
|
50 protected $_options = null; |
|
51 |
|
52 /** |
|
53 * The username of the account being authenticated. |
|
54 * |
|
55 * @var string |
|
56 */ |
|
57 protected $_username = null; |
|
58 |
|
59 /** |
|
60 * The password of the account being authenticated. |
|
61 * |
|
62 * @var string |
|
63 */ |
|
64 protected $_password = null; |
|
65 |
|
66 /** |
|
67 * The DN of the authenticated account. Used to retrieve the account entry on request. |
|
68 * |
|
69 * @var string |
|
70 */ |
|
71 protected $_authenticatedDn = null; |
|
72 |
|
73 /** |
|
74 * Constructor |
|
75 * |
|
76 * @param array $options An array of arrays of Zend_Ldap options |
|
77 * @param string $username The username of the account being authenticated |
|
78 * @param string $password The password of the account being authenticated |
|
79 * @return void |
|
80 */ |
|
81 public function __construct(array $options = array(), $username = null, $password = null) |
|
82 { |
|
83 $this->setOptions($options); |
|
84 if ($username !== null) { |
|
85 $this->setUsername($username); |
|
86 } |
|
87 if ($password !== null) { |
|
88 $this->setPassword($password); |
|
89 } |
|
90 } |
|
91 |
|
92 /** |
|
93 * Returns the array of arrays of Zend_Ldap options of this adapter. |
|
94 * |
|
95 * @return array|null |
|
96 */ |
|
97 public function getOptions() |
|
98 { |
|
99 return $this->_options; |
|
100 } |
|
101 |
|
102 /** |
|
103 * Sets the array of arrays of Zend_Ldap options to be used by |
|
104 * this adapter. |
|
105 * |
|
106 * @param array $options The array of arrays of Zend_Ldap options |
|
107 * @return Zend_Auth_Adapter_Ldap Provides a fluent interface |
|
108 */ |
|
109 public function setOptions($options) |
|
110 { |
|
111 $this->_options = is_array($options) ? $options : array(); |
|
112 return $this; |
|
113 } |
|
114 |
|
115 /** |
|
116 * Returns the username of the account being authenticated, or |
|
117 * NULL if none is set. |
|
118 * |
|
119 * @return string|null |
|
120 */ |
|
121 public function getUsername() |
|
122 { |
|
123 return $this->_username; |
|
124 } |
|
125 |
|
126 /** |
|
127 * Sets the username for binding |
|
128 * |
|
129 * @param string $username The username for binding |
|
130 * @return Zend_Auth_Adapter_Ldap Provides a fluent interface |
|
131 */ |
|
132 public function setUsername($username) |
|
133 { |
|
134 $this->_username = (string) $username; |
|
135 return $this; |
|
136 } |
|
137 |
|
138 /** |
|
139 * Returns the password of the account being authenticated, or |
|
140 * NULL if none is set. |
|
141 * |
|
142 * @return string|null |
|
143 */ |
|
144 public function getPassword() |
|
145 { |
|
146 return $this->_password; |
|
147 } |
|
148 |
|
149 /** |
|
150 * Sets the passwort for the account |
|
151 * |
|
152 * @param string $password The password of the account being authenticated |
|
153 * @return Zend_Auth_Adapter_Ldap Provides a fluent interface |
|
154 */ |
|
155 public function setPassword($password) |
|
156 { |
|
157 $this->_password = (string) $password; |
|
158 return $this; |
|
159 } |
|
160 |
|
161 /** |
|
162 * setIdentity() - set the identity (username) to be used |
|
163 * |
|
164 * Proxies to {@see setUsername()} |
|
165 * |
|
166 * Closes ZF-6813 |
|
167 * |
|
168 * @param string $identity |
|
169 * @return Zend_Auth_Adapter_Ldap Provides a fluent interface |
|
170 */ |
|
171 public function setIdentity($identity) |
|
172 { |
|
173 return $this->setUsername($identity); |
|
174 } |
|
175 |
|
176 /** |
|
177 * setCredential() - set the credential (password) value to be used |
|
178 * |
|
179 * Proxies to {@see setPassword()} |
|
180 * |
|
181 * Closes ZF-6813 |
|
182 * |
|
183 * @param string $credential |
|
184 * @return Zend_Auth_Adapter_Ldap Provides a fluent interface |
|
185 */ |
|
186 public function setCredential($credential) |
|
187 { |
|
188 return $this->setPassword($credential); |
|
189 } |
|
190 |
|
191 /** |
|
192 * Returns the LDAP Object |
|
193 * |
|
194 * @return Zend_Ldap The Zend_Ldap object used to authenticate the credentials |
|
195 */ |
|
196 public function getLdap() |
|
197 { |
|
198 if ($this->_ldap === null) { |
|
199 /** |
|
200 * @see Zend_Ldap |
|
201 */ |
|
202 require_once 'Zend/Ldap.php'; |
|
203 $this->_ldap = new Zend_Ldap(); |
|
204 } |
|
205 |
|
206 return $this->_ldap; |
|
207 } |
|
208 |
|
209 /** |
|
210 * Set an Ldap connection |
|
211 * |
|
212 * @param Zend_Ldap $ldap An existing Ldap object |
|
213 * @return Zend_Auth_Adapter_Ldap Provides a fluent interface |
|
214 */ |
|
215 public function setLdap(Zend_Ldap $ldap) |
|
216 { |
|
217 $this->_ldap = $ldap; |
|
218 |
|
219 $this->setOptions(array($ldap->getOptions())); |
|
220 |
|
221 return $this; |
|
222 } |
|
223 |
|
224 /** |
|
225 * Returns a domain name for the current LDAP options. This is used |
|
226 * for skipping redundant operations (e.g. authentications). |
|
227 * |
|
228 * @return string |
|
229 */ |
|
230 protected function _getAuthorityName() |
|
231 { |
|
232 $options = $this->getLdap()->getOptions(); |
|
233 $name = $options['accountDomainName']; |
|
234 if (!$name) |
|
235 $name = $options['accountDomainNameShort']; |
|
236 return $name ? $name : ''; |
|
237 } |
|
238 |
|
239 /** |
|
240 * Authenticate the user |
|
241 * |
|
242 * @throws Zend_Auth_Adapter_Exception |
|
243 * @return Zend_Auth_Result |
|
244 */ |
|
245 public function authenticate() |
|
246 { |
|
247 /** |
|
248 * @see Zend_Ldap_Exception |
|
249 */ |
|
250 require_once 'Zend/Ldap/Exception.php'; |
|
251 |
|
252 $messages = array(); |
|
253 $messages[0] = ''; // reserved |
|
254 $messages[1] = ''; // reserved |
|
255 |
|
256 $username = $this->_username; |
|
257 $password = $this->_password; |
|
258 |
|
259 if (!$username) { |
|
260 $code = Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND; |
|
261 $messages[0] = 'A username is required'; |
|
262 return new Zend_Auth_Result($code, '', $messages); |
|
263 } |
|
264 if (!$password) { |
|
265 /* A password is required because some servers will |
|
266 * treat an empty password as an anonymous bind. |
|
267 */ |
|
268 $code = Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID; |
|
269 $messages[0] = 'A password is required'; |
|
270 return new Zend_Auth_Result($code, '', $messages); |
|
271 } |
|
272 |
|
273 $ldap = $this->getLdap(); |
|
274 |
|
275 $code = Zend_Auth_Result::FAILURE; |
|
276 $messages[0] = "Authority not found: $username"; |
|
277 $failedAuthorities = array(); |
|
278 |
|
279 /* Iterate through each server and try to authenticate the supplied |
|
280 * credentials against it. |
|
281 */ |
|
282 foreach ($this->_options as $name => $options) { |
|
283 |
|
284 if (!is_array($options)) { |
|
285 /** |
|
286 * @see Zend_Auth_Adapter_Exception |
|
287 */ |
|
288 require_once 'Zend/Auth/Adapter/Exception.php'; |
|
289 throw new Zend_Auth_Adapter_Exception('Adapter options array not an array'); |
|
290 } |
|
291 $adapterOptions = $this->_prepareOptions($ldap, $options); |
|
292 $dname = ''; |
|
293 |
|
294 try { |
|
295 if ($messages[1]) |
|
296 $messages[] = $messages[1]; |
|
297 $messages[1] = ''; |
|
298 $messages[] = $this->_optionsToString($options); |
|
299 |
|
300 $dname = $this->_getAuthorityName(); |
|
301 if (isset($failedAuthorities[$dname])) { |
|
302 /* If multiple sets of server options for the same domain |
|
303 * are supplied, we want to skip redundant authentications |
|
304 * where the identity or credentials where found to be |
|
305 * invalid with another server for the same domain. The |
|
306 * $failedAuthorities array tracks this condition (and also |
|
307 * serves to supply the original error message). |
|
308 * This fixes issue ZF-4093. |
|
309 */ |
|
310 $messages[1] = $failedAuthorities[$dname]; |
|
311 $messages[] = "Skipping previously failed authority: $dname"; |
|
312 continue; |
|
313 } |
|
314 |
|
315 $canonicalName = $ldap->getCanonicalAccountName($username); |
|
316 $ldap->bind($canonicalName, $password); |
|
317 /* |
|
318 * Fixes problem when authenticated user is not allowed to retrieve |
|
319 * group-membership information or own account. |
|
320 * This requires that the user specified with "username" and optionally |
|
321 * "password" in the Zend_Ldap options is able to retrieve the required |
|
322 * information. |
|
323 */ |
|
324 $requireRebind = false; |
|
325 if (isset($options['username'])) { |
|
326 $ldap->bind(); |
|
327 $requireRebind = true; |
|
328 } |
|
329 $dn = $ldap->getCanonicalAccountName($canonicalName, Zend_Ldap::ACCTNAME_FORM_DN); |
|
330 |
|
331 $groupResult = $this->_checkGroupMembership($ldap, $canonicalName, $dn, $adapterOptions); |
|
332 if ($groupResult === true) { |
|
333 $this->_authenticatedDn = $dn; |
|
334 $messages[0] = ''; |
|
335 $messages[1] = ''; |
|
336 $messages[] = "$canonicalName authentication successful"; |
|
337 if ($requireRebind === true) { |
|
338 // rebinding with authenticated user |
|
339 $ldap->bind($dn, $password); |
|
340 } |
|
341 return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $canonicalName, $messages); |
|
342 } else { |
|
343 $messages[0] = 'Account is not a member of the specified group'; |
|
344 $messages[1] = $groupResult; |
|
345 $failedAuthorities[$dname] = $groupResult; |
|
346 } |
|
347 } catch (Zend_Ldap_Exception $zle) { |
|
348 |
|
349 /* LDAP based authentication is notoriously difficult to diagnose. Therefore |
|
350 * we bend over backwards to capture and record every possible bit of |
|
351 * information when something goes wrong. |
|
352 */ |
|
353 |
|
354 $err = $zle->getCode(); |
|
355 |
|
356 if ($err == Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH) { |
|
357 /* This error indicates that the domain supplied in the |
|
358 * username did not match the domains in the server options |
|
359 * and therefore we should just skip to the next set of |
|
360 * server options. |
|
361 */ |
|
362 continue; |
|
363 } else if ($err == Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT) { |
|
364 $code = Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND; |
|
365 $messages[0] = "Account not found: $username"; |
|
366 $failedAuthorities[$dname] = $zle->getMessage(); |
|
367 } else if ($err == Zend_Ldap_Exception::LDAP_INVALID_CREDENTIALS) { |
|
368 $code = Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID; |
|
369 $messages[0] = 'Invalid credentials'; |
|
370 $failedAuthorities[$dname] = $zle->getMessage(); |
|
371 } else { |
|
372 $line = $zle->getLine(); |
|
373 $messages[] = $zle->getFile() . "($line): " . $zle->getMessage(); |
|
374 $messages[] = str_replace($password, '*****', $zle->getTraceAsString()); |
|
375 $messages[0] = 'An unexpected failure occurred'; |
|
376 } |
|
377 $messages[1] = $zle->getMessage(); |
|
378 } |
|
379 } |
|
380 |
|
381 $msg = isset($messages[1]) ? $messages[1] : $messages[0]; |
|
382 $messages[] = "$username authentication failed: $msg"; |
|
383 |
|
384 return new Zend_Auth_Result($code, $username, $messages); |
|
385 } |
|
386 |
|
387 /** |
|
388 * Sets the LDAP specific options on the Zend_Ldap instance |
|
389 * |
|
390 * @param Zend_Ldap $ldap |
|
391 * @param array $options |
|
392 * @return array of auth-adapter specific options |
|
393 */ |
|
394 protected function _prepareOptions(Zend_Ldap $ldap, array $options) |
|
395 { |
|
396 $adapterOptions = array( |
|
397 'group' => null, |
|
398 'groupDn' => $ldap->getBaseDn(), |
|
399 'groupScope' => Zend_Ldap::SEARCH_SCOPE_SUB, |
|
400 'groupAttr' => 'cn', |
|
401 'groupFilter' => 'objectClass=groupOfUniqueNames', |
|
402 'memberAttr' => 'uniqueMember', |
|
403 'memberIsDn' => true |
|
404 ); |
|
405 foreach ($adapterOptions as $key => $value) { |
|
406 if (array_key_exists($key, $options)) { |
|
407 $value = $options[$key]; |
|
408 unset($options[$key]); |
|
409 switch ($key) { |
|
410 case 'groupScope': |
|
411 $value = (int)$value; |
|
412 if (in_array($value, array(Zend_Ldap::SEARCH_SCOPE_BASE, |
|
413 Zend_Ldap::SEARCH_SCOPE_ONE, Zend_Ldap::SEARCH_SCOPE_SUB), true)) { |
|
414 $adapterOptions[$key] = $value; |
|
415 } |
|
416 break; |
|
417 case 'memberIsDn': |
|
418 $adapterOptions[$key] = ($value === true || |
|
419 $value === '1' || strcasecmp($value, 'true') == 0); |
|
420 break; |
|
421 default: |
|
422 $adapterOptions[$key] = trim($value); |
|
423 break; |
|
424 } |
|
425 } |
|
426 } |
|
427 $ldap->setOptions($options); |
|
428 return $adapterOptions; |
|
429 } |
|
430 |
|
431 /** |
|
432 * Checks the group membership of the bound user |
|
433 * |
|
434 * @param Zend_Ldap $ldap |
|
435 * @param string $canonicalName |
|
436 * @param string $dn |
|
437 * @param array $adapterOptions |
|
438 * @return string|true |
|
439 */ |
|
440 protected function _checkGroupMembership(Zend_Ldap $ldap, $canonicalName, $dn, array $adapterOptions) |
|
441 { |
|
442 if ($adapterOptions['group'] === null) { |
|
443 return true; |
|
444 } |
|
445 |
|
446 if ($adapterOptions['memberIsDn'] === false) { |
|
447 $user = $canonicalName; |
|
448 } else { |
|
449 $user = $dn; |
|
450 } |
|
451 |
|
452 /** |
|
453 * @see Zend_Ldap_Filter |
|
454 */ |
|
455 require_once 'Zend/Ldap/Filter.php'; |
|
456 $groupName = Zend_Ldap_Filter::equals($adapterOptions['groupAttr'], $adapterOptions['group']); |
|
457 $membership = Zend_Ldap_Filter::equals($adapterOptions['memberAttr'], $user); |
|
458 $group = Zend_Ldap_Filter::andFilter($groupName, $membership); |
|
459 $groupFilter = $adapterOptions['groupFilter']; |
|
460 if (!empty($groupFilter)) { |
|
461 $group = $group->addAnd($groupFilter); |
|
462 } |
|
463 |
|
464 $result = $ldap->count($group, $adapterOptions['groupDn'], $adapterOptions['groupScope']); |
|
465 |
|
466 if ($result === 1) { |
|
467 return true; |
|
468 } else { |
|
469 return 'Failed to verify group membership with ' . $group->toString(); |
|
470 } |
|
471 } |
|
472 |
|
473 /** |
|
474 * getAccountObject() - Returns the result entry as a stdClass object |
|
475 * |
|
476 * This resembles the feature {@see Zend_Auth_Adapter_DbTable::getResultRowObject()}. |
|
477 * Closes ZF-6813 |
|
478 * |
|
479 * @param array $returnAttribs |
|
480 * @param array $omitAttribs |
|
481 * @return stdClass|boolean |
|
482 */ |
|
483 public function getAccountObject(array $returnAttribs = array(), array $omitAttribs = array()) |
|
484 { |
|
485 if (!$this->_authenticatedDn) { |
|
486 return false; |
|
487 } |
|
488 |
|
489 $returnObject = new stdClass(); |
|
490 |
|
491 $omitAttribs = array_map('strtolower', $omitAttribs); |
|
492 |
|
493 $entry = $this->getLdap()->getEntry($this->_authenticatedDn, $returnAttribs, true); |
|
494 foreach ($entry as $attr => $value) { |
|
495 if (in_array($attr, $omitAttribs)) { |
|
496 // skip attributes marked to be omitted |
|
497 continue; |
|
498 } |
|
499 if (is_array($value)) { |
|
500 $returnObject->$attr = (count($value) > 1) ? $value : $value[0]; |
|
501 } else { |
|
502 $returnObject->$attr = $value; |
|
503 } |
|
504 } |
|
505 return $returnObject; |
|
506 } |
|
507 |
|
508 /** |
|
509 * Converts options to string |
|
510 * |
|
511 * @param array $options |
|
512 * @return string |
|
513 */ |
|
514 private function _optionsToString(array $options) |
|
515 { |
|
516 $str = ''; |
|
517 foreach ($options as $key => $val) { |
|
518 if ($key === 'password') |
|
519 $val = '*****'; |
|
520 if ($str) |
|
521 $str .= ','; |
|
522 $str .= $key . '=' . $val; |
|
523 } |
|
524 return $str; |
|
525 } |
|
526 } |