|
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_Http |
|
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: Http.php 23088 2010-10-11 19:53:24Z padraic $ |
|
21 */ |
|
22 |
|
23 |
|
24 /** |
|
25 * @see Zend_Auth_Adapter_Interface |
|
26 */ |
|
27 require_once 'Zend/Auth/Adapter/Interface.php'; |
|
28 |
|
29 |
|
30 /** |
|
31 * HTTP Authentication Adapter |
|
32 * |
|
33 * Implements a pretty good chunk of RFC 2617. |
|
34 * |
|
35 * @category Zend |
|
36 * @package Zend_Auth |
|
37 * @subpackage Zend_Auth_Adapter_Http |
|
38 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
39 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
40 * @todo Support auth-int |
|
41 * @todo Track nonces, nonce-count, opaque for replay protection and stale support |
|
42 * @todo Support Authentication-Info header |
|
43 */ |
|
44 class Zend_Auth_Adapter_Http implements Zend_Auth_Adapter_Interface |
|
45 { |
|
46 /** |
|
47 * Reference to the HTTP Request object |
|
48 * |
|
49 * @var Zend_Controller_Request_Http |
|
50 */ |
|
51 protected $_request; |
|
52 |
|
53 /** |
|
54 * Reference to the HTTP Response object |
|
55 * |
|
56 * @var Zend_Controller_Response_Http |
|
57 */ |
|
58 protected $_response; |
|
59 |
|
60 /** |
|
61 * Object that looks up user credentials for the Basic scheme |
|
62 * |
|
63 * @var Zend_Auth_Adapter_Http_Resolver_Interface |
|
64 */ |
|
65 protected $_basicResolver; |
|
66 |
|
67 /** |
|
68 * Object that looks up user credentials for the Digest scheme |
|
69 * |
|
70 * @var Zend_Auth_Adapter_Http_Resolver_Interface |
|
71 */ |
|
72 protected $_digestResolver; |
|
73 |
|
74 /** |
|
75 * List of authentication schemes supported by this class |
|
76 * |
|
77 * @var array |
|
78 */ |
|
79 protected $_supportedSchemes = array('basic', 'digest'); |
|
80 |
|
81 /** |
|
82 * List of schemes this class will accept from the client |
|
83 * |
|
84 * @var array |
|
85 */ |
|
86 protected $_acceptSchemes; |
|
87 |
|
88 /** |
|
89 * Space-delimited list of protected domains for Digest Auth |
|
90 * |
|
91 * @var string |
|
92 */ |
|
93 protected $_domains; |
|
94 |
|
95 /** |
|
96 * The protection realm to use |
|
97 * |
|
98 * @var string |
|
99 */ |
|
100 protected $_realm; |
|
101 |
|
102 /** |
|
103 * Nonce timeout period |
|
104 * |
|
105 * @var integer |
|
106 */ |
|
107 protected $_nonceTimeout; |
|
108 |
|
109 /** |
|
110 * Whether to send the opaque value in the header. True by default |
|
111 * |
|
112 * @var boolean |
|
113 */ |
|
114 protected $_useOpaque; |
|
115 |
|
116 /** |
|
117 * List of the supported digest algorithms. I want to support both MD5 and |
|
118 * MD5-sess, but MD5-sess won't make it into the first version. |
|
119 * |
|
120 * @var array |
|
121 */ |
|
122 protected $_supportedAlgos = array('MD5'); |
|
123 |
|
124 /** |
|
125 * The actual algorithm to use. Defaults to MD5 |
|
126 * |
|
127 * @var string |
|
128 */ |
|
129 protected $_algo; |
|
130 |
|
131 /** |
|
132 * List of supported qop options. My intetion is to support both 'auth' and |
|
133 * 'auth-int', but 'auth-int' won't make it into the first version. |
|
134 * |
|
135 * @var array |
|
136 */ |
|
137 protected $_supportedQops = array('auth'); |
|
138 |
|
139 /** |
|
140 * Whether or not to do Proxy Authentication instead of origin server |
|
141 * authentication (send 407's instead of 401's). Off by default. |
|
142 * |
|
143 * @var boolean |
|
144 */ |
|
145 protected $_imaProxy; |
|
146 |
|
147 /** |
|
148 * Flag indicating the client is IE and didn't bother to return the opaque string |
|
149 * |
|
150 * @var boolean |
|
151 */ |
|
152 protected $_ieNoOpaque; |
|
153 |
|
154 /** |
|
155 * Constructor |
|
156 * |
|
157 * @param array $config Configuration settings: |
|
158 * 'accept_schemes' => 'basic'|'digest'|'basic digest' |
|
159 * 'realm' => <string> |
|
160 * 'digest_domains' => <string> Space-delimited list of URIs |
|
161 * 'nonce_timeout' => <int> |
|
162 * 'use_opaque' => <bool> Whether to send the opaque value in the header |
|
163 * 'alogrithm' => <string> See $_supportedAlgos. Default: MD5 |
|
164 * 'proxy_auth' => <bool> Whether to do authentication as a Proxy |
|
165 * @throws Zend_Auth_Adapter_Exception |
|
166 * @return void |
|
167 */ |
|
168 public function __construct(array $config) |
|
169 { |
|
170 if (!extension_loaded('hash')) { |
|
171 /** |
|
172 * @see Zend_Auth_Adapter_Exception |
|
173 */ |
|
174 require_once 'Zend/Auth/Adapter/Exception.php'; |
|
175 throw new Zend_Auth_Adapter_Exception(__CLASS__ . ' requires the \'hash\' extension'); |
|
176 } |
|
177 |
|
178 $this->_request = null; |
|
179 $this->_response = null; |
|
180 $this->_ieNoOpaque = false; |
|
181 |
|
182 |
|
183 if (empty($config['accept_schemes'])) { |
|
184 /** |
|
185 * @see Zend_Auth_Adapter_Exception |
|
186 */ |
|
187 require_once 'Zend/Auth/Adapter/Exception.php'; |
|
188 throw new Zend_Auth_Adapter_Exception('Config key \'accept_schemes\' is required'); |
|
189 } |
|
190 |
|
191 $schemes = explode(' ', $config['accept_schemes']); |
|
192 $this->_acceptSchemes = array_intersect($schemes, $this->_supportedSchemes); |
|
193 if (empty($this->_acceptSchemes)) { |
|
194 /** |
|
195 * @see Zend_Auth_Adapter_Exception |
|
196 */ |
|
197 require_once 'Zend/Auth/Adapter/Exception.php'; |
|
198 throw new Zend_Auth_Adapter_Exception('No supported schemes given in \'accept_schemes\'. Valid values: ' |
|
199 . implode(', ', $this->_supportedSchemes)); |
|
200 } |
|
201 |
|
202 // Double-quotes are used to delimit the realm string in the HTTP header, |
|
203 // and colons are field delimiters in the password file. |
|
204 if (empty($config['realm']) || |
|
205 !ctype_print($config['realm']) || |
|
206 strpos($config['realm'], ':') !== false || |
|
207 strpos($config['realm'], '"') !== false) { |
|
208 /** |
|
209 * @see Zend_Auth_Adapter_Exception |
|
210 */ |
|
211 require_once 'Zend/Auth/Adapter/Exception.php'; |
|
212 throw new Zend_Auth_Adapter_Exception('Config key \'realm\' is required, and must contain only printable ' |
|
213 . 'characters, excluding quotation marks and colons'); |
|
214 } else { |
|
215 $this->_realm = $config['realm']; |
|
216 } |
|
217 |
|
218 if (in_array('digest', $this->_acceptSchemes)) { |
|
219 if (empty($config['digest_domains']) || |
|
220 !ctype_print($config['digest_domains']) || |
|
221 strpos($config['digest_domains'], '"') !== false) { |
|
222 /** |
|
223 * @see Zend_Auth_Adapter_Exception |
|
224 */ |
|
225 require_once 'Zend/Auth/Adapter/Exception.php'; |
|
226 throw new Zend_Auth_Adapter_Exception('Config key \'digest_domains\' is required, and must contain ' |
|
227 . 'only printable characters, excluding quotation marks'); |
|
228 } else { |
|
229 $this->_domains = $config['digest_domains']; |
|
230 } |
|
231 |
|
232 if (empty($config['nonce_timeout']) || |
|
233 !is_numeric($config['nonce_timeout'])) { |
|
234 /** |
|
235 * @see Zend_Auth_Adapter_Exception |
|
236 */ |
|
237 require_once 'Zend/Auth/Adapter/Exception.php'; |
|
238 throw new Zend_Auth_Adapter_Exception('Config key \'nonce_timeout\' is required, and must be an ' |
|
239 . 'integer'); |
|
240 } else { |
|
241 $this->_nonceTimeout = (int) $config['nonce_timeout']; |
|
242 } |
|
243 |
|
244 // We use the opaque value unless explicitly told not to |
|
245 if (isset($config['use_opaque']) && false == (bool) $config['use_opaque']) { |
|
246 $this->_useOpaque = false; |
|
247 } else { |
|
248 $this->_useOpaque = true; |
|
249 } |
|
250 |
|
251 if (isset($config['algorithm']) && in_array($config['algorithm'], $this->_supportedAlgos)) { |
|
252 $this->_algo = $config['algorithm']; |
|
253 } else { |
|
254 $this->_algo = 'MD5'; |
|
255 } |
|
256 } |
|
257 |
|
258 // Don't be a proxy unless explicitly told to do so |
|
259 if (isset($config['proxy_auth']) && true == (bool) $config['proxy_auth']) { |
|
260 $this->_imaProxy = true; // I'm a Proxy |
|
261 } else { |
|
262 $this->_imaProxy = false; |
|
263 } |
|
264 } |
|
265 |
|
266 /** |
|
267 * Setter for the _basicResolver property |
|
268 * |
|
269 * @param Zend_Auth_Adapter_Http_Resolver_Interface $resolver |
|
270 * @return Zend_Auth_Adapter_Http Provides a fluent interface |
|
271 */ |
|
272 public function setBasicResolver(Zend_Auth_Adapter_Http_Resolver_Interface $resolver) |
|
273 { |
|
274 $this->_basicResolver = $resolver; |
|
275 |
|
276 return $this; |
|
277 } |
|
278 |
|
279 /** |
|
280 * Getter for the _basicResolver property |
|
281 * |
|
282 * @return Zend_Auth_Adapter_Http_Resolver_Interface |
|
283 */ |
|
284 public function getBasicResolver() |
|
285 { |
|
286 return $this->_basicResolver; |
|
287 } |
|
288 |
|
289 /** |
|
290 * Setter for the _digestResolver property |
|
291 * |
|
292 * @param Zend_Auth_Adapter_Http_Resolver_Interface $resolver |
|
293 * @return Zend_Auth_Adapter_Http Provides a fluent interface |
|
294 */ |
|
295 public function setDigestResolver(Zend_Auth_Adapter_Http_Resolver_Interface $resolver) |
|
296 { |
|
297 $this->_digestResolver = $resolver; |
|
298 |
|
299 return $this; |
|
300 } |
|
301 |
|
302 /** |
|
303 * Getter for the _digestResolver property |
|
304 * |
|
305 * @return Zend_Auth_Adapter_Http_Resolver_Interface |
|
306 */ |
|
307 public function getDigestResolver() |
|
308 { |
|
309 return $this->_digestResolver; |
|
310 } |
|
311 |
|
312 /** |
|
313 * Setter for the Request object |
|
314 * |
|
315 * @param Zend_Controller_Request_Http $request |
|
316 * @return Zend_Auth_Adapter_Http Provides a fluent interface |
|
317 */ |
|
318 public function setRequest(Zend_Controller_Request_Http $request) |
|
319 { |
|
320 $this->_request = $request; |
|
321 |
|
322 return $this; |
|
323 } |
|
324 |
|
325 /** |
|
326 * Getter for the Request object |
|
327 * |
|
328 * @return Zend_Controller_Request_Http |
|
329 */ |
|
330 public function getRequest() |
|
331 { |
|
332 return $this->_request; |
|
333 } |
|
334 |
|
335 /** |
|
336 * Setter for the Response object |
|
337 * |
|
338 * @param Zend_Controller_Response_Http $response |
|
339 * @return Zend_Auth_Adapter_Http Provides a fluent interface |
|
340 */ |
|
341 public function setResponse(Zend_Controller_Response_Http $response) |
|
342 { |
|
343 $this->_response = $response; |
|
344 |
|
345 return $this; |
|
346 } |
|
347 |
|
348 /** |
|
349 * Getter for the Response object |
|
350 * |
|
351 * @return Zend_Controller_Response_Http |
|
352 */ |
|
353 public function getResponse() |
|
354 { |
|
355 return $this->_response; |
|
356 } |
|
357 |
|
358 /** |
|
359 * Authenticate |
|
360 * |
|
361 * @throws Zend_Auth_Adapter_Exception |
|
362 * @return Zend_Auth_Result |
|
363 */ |
|
364 public function authenticate() |
|
365 { |
|
366 if (empty($this->_request) || |
|
367 empty($this->_response)) { |
|
368 /** |
|
369 * @see Zend_Auth_Adapter_Exception |
|
370 */ |
|
371 require_once 'Zend/Auth/Adapter/Exception.php'; |
|
372 throw new Zend_Auth_Adapter_Exception('Request and Response objects must be set before calling ' |
|
373 . 'authenticate()'); |
|
374 } |
|
375 |
|
376 if ($this->_imaProxy) { |
|
377 $getHeader = 'Proxy-Authorization'; |
|
378 } else { |
|
379 $getHeader = 'Authorization'; |
|
380 } |
|
381 |
|
382 $authHeader = $this->_request->getHeader($getHeader); |
|
383 if (!$authHeader) { |
|
384 return $this->_challengeClient(); |
|
385 } |
|
386 |
|
387 list($clientScheme) = explode(' ', $authHeader); |
|
388 $clientScheme = strtolower($clientScheme); |
|
389 |
|
390 // The server can issue multiple challenges, but the client should |
|
391 // answer with only the selected auth scheme. |
|
392 if (!in_array($clientScheme, $this->_supportedSchemes)) { |
|
393 $this->_response->setHttpResponseCode(400); |
|
394 return new Zend_Auth_Result( |
|
395 Zend_Auth_Result::FAILURE_UNCATEGORIZED, |
|
396 array(), |
|
397 array('Client requested an incorrect or unsupported authentication scheme') |
|
398 ); |
|
399 } |
|
400 |
|
401 // client sent a scheme that is not the one required |
|
402 if (!in_array($clientScheme, $this->_acceptSchemes)) { |
|
403 // challenge again the client |
|
404 return $this->_challengeClient(); |
|
405 } |
|
406 |
|
407 switch ($clientScheme) { |
|
408 case 'basic': |
|
409 $result = $this->_basicAuth($authHeader); |
|
410 break; |
|
411 case 'digest': |
|
412 $result = $this->_digestAuth($authHeader); |
|
413 break; |
|
414 default: |
|
415 /** |
|
416 * @see Zend_Auth_Adapter_Exception |
|
417 */ |
|
418 require_once 'Zend/Auth/Adapter/Exception.php'; |
|
419 throw new Zend_Auth_Adapter_Exception('Unsupported authentication scheme'); |
|
420 } |
|
421 |
|
422 return $result; |
|
423 } |
|
424 |
|
425 /** |
|
426 * Challenge Client |
|
427 * |
|
428 * Sets a 401 or 407 Unauthorized response code, and creates the |
|
429 * appropriate Authenticate header(s) to prompt for credentials. |
|
430 * |
|
431 * @return Zend_Auth_Result Always returns a non-identity Auth result |
|
432 */ |
|
433 protected function _challengeClient() |
|
434 { |
|
435 if ($this->_imaProxy) { |
|
436 $statusCode = 407; |
|
437 $headerName = 'Proxy-Authenticate'; |
|
438 } else { |
|
439 $statusCode = 401; |
|
440 $headerName = 'WWW-Authenticate'; |
|
441 } |
|
442 |
|
443 $this->_response->setHttpResponseCode($statusCode); |
|
444 |
|
445 // Send a challenge in each acceptable authentication scheme |
|
446 if (in_array('basic', $this->_acceptSchemes)) { |
|
447 $this->_response->setHeader($headerName, $this->_basicHeader()); |
|
448 } |
|
449 if (in_array('digest', $this->_acceptSchemes)) { |
|
450 $this->_response->setHeader($headerName, $this->_digestHeader()); |
|
451 } |
|
452 return new Zend_Auth_Result( |
|
453 Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID, |
|
454 array(), |
|
455 array('Invalid or absent credentials; challenging client') |
|
456 ); |
|
457 } |
|
458 |
|
459 /** |
|
460 * Basic Header |
|
461 * |
|
462 * Generates a Proxy- or WWW-Authenticate header value in the Basic |
|
463 * authentication scheme. |
|
464 * |
|
465 * @return string Authenticate header value |
|
466 */ |
|
467 protected function _basicHeader() |
|
468 { |
|
469 return 'Basic realm="' . $this->_realm . '"'; |
|
470 } |
|
471 |
|
472 /** |
|
473 * Digest Header |
|
474 * |
|
475 * Generates a Proxy- or WWW-Authenticate header value in the Digest |
|
476 * authentication scheme. |
|
477 * |
|
478 * @return string Authenticate header value |
|
479 */ |
|
480 protected function _digestHeader() |
|
481 { |
|
482 $wwwauth = 'Digest realm="' . $this->_realm . '", ' |
|
483 . 'domain="' . $this->_domains . '", ' |
|
484 . 'nonce="' . $this->_calcNonce() . '", ' |
|
485 . ($this->_useOpaque ? 'opaque="' . $this->_calcOpaque() . '", ' : '') |
|
486 . 'algorithm="' . $this->_algo . '", ' |
|
487 . 'qop="' . implode(',', $this->_supportedQops) . '"'; |
|
488 |
|
489 return $wwwauth; |
|
490 } |
|
491 |
|
492 /** |
|
493 * Basic Authentication |
|
494 * |
|
495 * @param string $header Client's Authorization header |
|
496 * @throws Zend_Auth_Adapter_Exception |
|
497 * @return Zend_Auth_Result |
|
498 */ |
|
499 protected function _basicAuth($header) |
|
500 { |
|
501 if (empty($header)) { |
|
502 /** |
|
503 * @see Zend_Auth_Adapter_Exception |
|
504 */ |
|
505 require_once 'Zend/Auth/Adapter/Exception.php'; |
|
506 throw new Zend_Auth_Adapter_Exception('The value of the client Authorization header is required'); |
|
507 } |
|
508 if (empty($this->_basicResolver)) { |
|
509 /** |
|
510 * @see Zend_Auth_Adapter_Exception |
|
511 */ |
|
512 require_once 'Zend/Auth/Adapter/Exception.php'; |
|
513 throw new Zend_Auth_Adapter_Exception('A basicResolver object must be set before doing Basic ' |
|
514 . 'authentication'); |
|
515 } |
|
516 |
|
517 // Decode the Authorization header |
|
518 $auth = substr($header, strlen('Basic ')); |
|
519 $auth = base64_decode($auth); |
|
520 if (!$auth) { |
|
521 /** |
|
522 * @see Zend_Auth_Adapter_Exception |
|
523 */ |
|
524 require_once 'Zend/Auth/Adapter/Exception.php'; |
|
525 throw new Zend_Auth_Adapter_Exception('Unable to base64_decode Authorization header value'); |
|
526 } |
|
527 |
|
528 // See ZF-1253. Validate the credentials the same way the digest |
|
529 // implementation does. If invalid credentials are detected, |
|
530 // re-challenge the client. |
|
531 if (!ctype_print($auth)) { |
|
532 return $this->_challengeClient(); |
|
533 } |
|
534 // Fix for ZF-1515: Now re-challenges on empty username or password |
|
535 $creds = array_filter(explode(':', $auth)); |
|
536 if (count($creds) != 2) { |
|
537 return $this->_challengeClient(); |
|
538 } |
|
539 |
|
540 $password = $this->_basicResolver->resolve($creds[0], $this->_realm); |
|
541 if ($password && $this->_secureStringCompare($password, $creds[1])) { |
|
542 $identity = array('username'=>$creds[0], 'realm'=>$this->_realm); |
|
543 return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $identity); |
|
544 } else { |
|
545 return $this->_challengeClient(); |
|
546 } |
|
547 } |
|
548 |
|
549 /** |
|
550 * Digest Authentication |
|
551 * |
|
552 * @param string $header Client's Authorization header |
|
553 * @throws Zend_Auth_Adapter_Exception |
|
554 * @return Zend_Auth_Result Valid auth result only on successful auth |
|
555 */ |
|
556 protected function _digestAuth($header) |
|
557 { |
|
558 if (empty($header)) { |
|
559 /** |
|
560 * @see Zend_Auth_Adapter_Exception |
|
561 */ |
|
562 require_once 'Zend/Auth/Adapter/Exception.php'; |
|
563 throw new Zend_Auth_Adapter_Exception('The value of the client Authorization header is required'); |
|
564 } |
|
565 if (empty($this->_digestResolver)) { |
|
566 /** |
|
567 * @see Zend_Auth_Adapter_Exception |
|
568 */ |
|
569 require_once 'Zend/Auth/Adapter/Exception.php'; |
|
570 throw new Zend_Auth_Adapter_Exception('A digestResolver object must be set before doing Digest authentication'); |
|
571 } |
|
572 |
|
573 $data = $this->_parseDigestAuth($header); |
|
574 if ($data === false) { |
|
575 $this->_response->setHttpResponseCode(400); |
|
576 return new Zend_Auth_Result( |
|
577 Zend_Auth_Result::FAILURE_UNCATEGORIZED, |
|
578 array(), |
|
579 array('Invalid Authorization header format') |
|
580 ); |
|
581 } |
|
582 |
|
583 // See ZF-1052. This code was a bit too unforgiving of invalid |
|
584 // usernames. Now, if the username is bad, we re-challenge the client. |
|
585 if ('::invalid::' == $data['username']) { |
|
586 return $this->_challengeClient(); |
|
587 } |
|
588 |
|
589 // Verify that the client sent back the same nonce |
|
590 if ($this->_calcNonce() != $data['nonce']) { |
|
591 return $this->_challengeClient(); |
|
592 } |
|
593 // The opaque value is also required to match, but of course IE doesn't |
|
594 // play ball. |
|
595 if (!$this->_ieNoOpaque && $this->_calcOpaque() != $data['opaque']) { |
|
596 return $this->_challengeClient(); |
|
597 } |
|
598 |
|
599 // Look up the user's password hash. If not found, deny access. |
|
600 // This makes no assumptions about how the password hash was |
|
601 // constructed beyond that it must have been built in such a way as |
|
602 // to be recreatable with the current settings of this object. |
|
603 $ha1 = $this->_digestResolver->resolve($data['username'], $data['realm']); |
|
604 if ($ha1 === false) { |
|
605 return $this->_challengeClient(); |
|
606 } |
|
607 |
|
608 // If MD5-sess is used, a1 value is made of the user's password |
|
609 // hash with the server and client nonce appended, separated by |
|
610 // colons. |
|
611 if ($this->_algo == 'MD5-sess') { |
|
612 $ha1 = hash('md5', $ha1 . ':' . $data['nonce'] . ':' . $data['cnonce']); |
|
613 } |
|
614 |
|
615 // Calculate h(a2). The value of this hash depends on the qop |
|
616 // option selected by the client and the supported hash functions |
|
617 switch ($data['qop']) { |
|
618 case 'auth': |
|
619 $a2 = $this->_request->getMethod() . ':' . $data['uri']; |
|
620 break; |
|
621 case 'auth-int': |
|
622 // Should be REQUEST_METHOD . ':' . uri . ':' . hash(entity-body), |
|
623 // but this isn't supported yet, so fall through to default case |
|
624 default: |
|
625 /** |
|
626 * @see Zend_Auth_Adapter_Exception |
|
627 */ |
|
628 require_once 'Zend/Auth/Adapter/Exception.php'; |
|
629 throw new Zend_Auth_Adapter_Exception('Client requested an unsupported qop option'); |
|
630 } |
|
631 // Using hash() should make parameterizing the hash algorithm |
|
632 // easier |
|
633 $ha2 = hash('md5', $a2); |
|
634 |
|
635 |
|
636 // Calculate the server's version of the request-digest. This must |
|
637 // match $data['response']. See RFC 2617, section 3.2.2.1 |
|
638 $message = $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $ha2; |
|
639 $digest = hash('md5', $ha1 . ':' . $message); |
|
640 |
|
641 // If our digest matches the client's let them in, otherwise return |
|
642 // a 401 code and exit to prevent access to the protected resource. |
|
643 if ($this->_secureStringCompare($digest, $data['response'])) { |
|
644 $identity = array('username'=>$data['username'], 'realm'=>$data['realm']); |
|
645 return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $identity); |
|
646 } else { |
|
647 return $this->_challengeClient(); |
|
648 } |
|
649 } |
|
650 |
|
651 /** |
|
652 * Calculate Nonce |
|
653 * |
|
654 * @return string The nonce value |
|
655 */ |
|
656 protected function _calcNonce() |
|
657 { |
|
658 // Once subtle consequence of this timeout calculation is that it |
|
659 // actually divides all of time into _nonceTimeout-sized sections, such |
|
660 // that the value of timeout is the point in time of the next |
|
661 // approaching "boundary" of a section. This allows the server to |
|
662 // consistently generate the same timeout (and hence the same nonce |
|
663 // value) across requests, but only as long as one of those |
|
664 // "boundaries" is not crossed between requests. If that happens, the |
|
665 // nonce will change on its own, and effectively log the user out. This |
|
666 // would be surprising if the user just logged in. |
|
667 $timeout = ceil(time() / $this->_nonceTimeout) * $this->_nonceTimeout; |
|
668 |
|
669 $nonce = hash('md5', $timeout . ':' . $this->_request->getServer('HTTP_USER_AGENT') . ':' . __CLASS__); |
|
670 return $nonce; |
|
671 } |
|
672 |
|
673 /** |
|
674 * Calculate Opaque |
|
675 * |
|
676 * The opaque string can be anything; the client must return it exactly as |
|
677 * it was sent. It may be useful to store data in this string in some |
|
678 * applications. Ideally, a new value for this would be generated each time |
|
679 * a WWW-Authenticate header is sent (in order to reduce predictability), |
|
680 * but we would have to be able to create the same exact value across at |
|
681 * least two separate requests from the same client. |
|
682 * |
|
683 * @return string The opaque value |
|
684 */ |
|
685 protected function _calcOpaque() |
|
686 { |
|
687 return hash('md5', 'Opaque Data:' . __CLASS__); |
|
688 } |
|
689 |
|
690 /** |
|
691 * Parse Digest Authorization header |
|
692 * |
|
693 * @param string $header Client's Authorization: HTTP header |
|
694 * @return array|false Data elements from header, or false if any part of |
|
695 * the header is invalid |
|
696 */ |
|
697 protected function _parseDigestAuth($header) |
|
698 { |
|
699 $temp = null; |
|
700 $data = array(); |
|
701 |
|
702 // See ZF-1052. Detect invalid usernames instead of just returning a |
|
703 // 400 code. |
|
704 $ret = preg_match('/username="([^"]+)"/', $header, $temp); |
|
705 if (!$ret || empty($temp[1]) |
|
706 || !ctype_print($temp[1]) |
|
707 || strpos($temp[1], ':') !== false) { |
|
708 $data['username'] = '::invalid::'; |
|
709 } else { |
|
710 $data['username'] = $temp[1]; |
|
711 } |
|
712 $temp = null; |
|
713 |
|
714 $ret = preg_match('/realm="([^"]+)"/', $header, $temp); |
|
715 if (!$ret || empty($temp[1])) { |
|
716 return false; |
|
717 } |
|
718 if (!ctype_print($temp[1]) || strpos($temp[1], ':') !== false) { |
|
719 return false; |
|
720 } else { |
|
721 $data['realm'] = $temp[1]; |
|
722 } |
|
723 $temp = null; |
|
724 |
|
725 $ret = preg_match('/nonce="([^"]+)"/', $header, $temp); |
|
726 if (!$ret || empty($temp[1])) { |
|
727 return false; |
|
728 } |
|
729 if (!ctype_xdigit($temp[1])) { |
|
730 return false; |
|
731 } else { |
|
732 $data['nonce'] = $temp[1]; |
|
733 } |
|
734 $temp = null; |
|
735 |
|
736 $ret = preg_match('/uri="([^"]+)"/', $header, $temp); |
|
737 if (!$ret || empty($temp[1])) { |
|
738 return false; |
|
739 } |
|
740 // Section 3.2.2.5 in RFC 2617 says the authenticating server must |
|
741 // verify that the URI field in the Authorization header is for the |
|
742 // same resource requested in the Request Line. |
|
743 $rUri = @parse_url($this->_request->getRequestUri()); |
|
744 $cUri = @parse_url($temp[1]); |
|
745 if (false === $rUri || false === $cUri) { |
|
746 return false; |
|
747 } else { |
|
748 // Make sure the path portion of both URIs is the same |
|
749 if ($rUri['path'] != $cUri['path']) { |
|
750 return false; |
|
751 } |
|
752 // Section 3.2.2.5 seems to suggest that the value of the URI |
|
753 // Authorization field should be made into an absolute URI if the |
|
754 // Request URI is absolute, but it's vague, and that's a bunch of |
|
755 // code I don't want to write right now. |
|
756 $data['uri'] = $temp[1]; |
|
757 } |
|
758 $temp = null; |
|
759 |
|
760 $ret = preg_match('/response="([^"]+)"/', $header, $temp); |
|
761 if (!$ret || empty($temp[1])) { |
|
762 return false; |
|
763 } |
|
764 if (32 != strlen($temp[1]) || !ctype_xdigit($temp[1])) { |
|
765 return false; |
|
766 } else { |
|
767 $data['response'] = $temp[1]; |
|
768 } |
|
769 $temp = null; |
|
770 |
|
771 // The spec says this should default to MD5 if omitted. OK, so how does |
|
772 // that square with the algo we send out in the WWW-Authenticate header, |
|
773 // if it can easily be overridden by the client? |
|
774 $ret = preg_match('/algorithm="?(' . $this->_algo . ')"?/', $header, $temp); |
|
775 if ($ret && !empty($temp[1]) |
|
776 && in_array($temp[1], $this->_supportedAlgos)) { |
|
777 $data['algorithm'] = $temp[1]; |
|
778 } else { |
|
779 $data['algorithm'] = 'MD5'; // = $this->_algo; ? |
|
780 } |
|
781 $temp = null; |
|
782 |
|
783 // Not optional in this implementation |
|
784 $ret = preg_match('/cnonce="([^"]+)"/', $header, $temp); |
|
785 if (!$ret || empty($temp[1])) { |
|
786 return false; |
|
787 } |
|
788 if (!ctype_print($temp[1])) { |
|
789 return false; |
|
790 } else { |
|
791 $data['cnonce'] = $temp[1]; |
|
792 } |
|
793 $temp = null; |
|
794 |
|
795 // If the server sent an opaque value, the client must send it back |
|
796 if ($this->_useOpaque) { |
|
797 $ret = preg_match('/opaque="([^"]+)"/', $header, $temp); |
|
798 if (!$ret || empty($temp[1])) { |
|
799 |
|
800 // Big surprise: IE isn't RFC 2617-compliant. |
|
801 if (false !== strpos($this->_request->getHeader('User-Agent'), 'MSIE')) { |
|
802 $temp[1] = ''; |
|
803 $this->_ieNoOpaque = true; |
|
804 } else { |
|
805 return false; |
|
806 } |
|
807 } |
|
808 // This implementation only sends MD5 hex strings in the opaque value |
|
809 if (!$this->_ieNoOpaque && |
|
810 (32 != strlen($temp[1]) || !ctype_xdigit($temp[1]))) { |
|
811 return false; |
|
812 } else { |
|
813 $data['opaque'] = $temp[1]; |
|
814 } |
|
815 $temp = null; |
|
816 } |
|
817 |
|
818 // Not optional in this implementation, but must be one of the supported |
|
819 // qop types |
|
820 $ret = preg_match('/qop="?(' . implode('|', $this->_supportedQops) . ')"?/', $header, $temp); |
|
821 if (!$ret || empty($temp[1])) { |
|
822 return false; |
|
823 } |
|
824 if (!in_array($temp[1], $this->_supportedQops)) { |
|
825 return false; |
|
826 } else { |
|
827 $data['qop'] = $temp[1]; |
|
828 } |
|
829 $temp = null; |
|
830 |
|
831 // Not optional in this implementation. The spec says this value |
|
832 // shouldn't be a quoted string, but apparently some implementations |
|
833 // quote it anyway. See ZF-1544. |
|
834 $ret = preg_match('/nc="?([0-9A-Fa-f]{8})"?/', $header, $temp); |
|
835 if (!$ret || empty($temp[1])) { |
|
836 return false; |
|
837 } |
|
838 if (8 != strlen($temp[1]) || !ctype_xdigit($temp[1])) { |
|
839 return false; |
|
840 } else { |
|
841 $data['nc'] = $temp[1]; |
|
842 } |
|
843 $temp = null; |
|
844 |
|
845 return $data; |
|
846 } |
|
847 |
|
848 /** |
|
849 * Securely compare two strings for equality while avoided C level memcmp() |
|
850 * optimisations capable of leaking timing information useful to an attacker |
|
851 * attempting to iteratively guess the unknown string (e.g. password) being |
|
852 * compared against. |
|
853 * |
|
854 * @param string $a |
|
855 * @param string $b |
|
856 * @return bool |
|
857 */ |
|
858 protected function _secureStringCompare($a, $b) |
|
859 { |
|
860 if (strlen($a) !== strlen($b)) { |
|
861 return false; |
|
862 } |
|
863 $result = 0; |
|
864 for ($i = 0; $i < strlen($a); $i++) { |
|
865 $result |= ord($a[$i]) ^ ord($b[$i]); |
|
866 } |
|
867 return $result == 0; |
|
868 } |
|
869 } |