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_Http 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 /** 0025 * @see Zend_Auth_Adapter_Interface 0026 */ 0027 // require_once 'Zend/Auth/Adapter/Interface.php'; 0028 0029 0030 /** 0031 * HTTP Authentication Adapter 0032 * 0033 * Implements a pretty good chunk of RFC 2617. 0034 * 0035 * @category Zend 0036 * @package Zend_Auth 0037 * @subpackage Zend_Auth_Adapter_Http 0038 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 0039 * @license http://framework.zend.com/license/new-bsd New BSD License 0040 * @todo Support auth-int 0041 * @todo Track nonces, nonce-count, opaque for replay protection and stale support 0042 * @todo Support Authentication-Info header 0043 */ 0044 class Zend_Auth_Adapter_Http implements Zend_Auth_Adapter_Interface 0045 { 0046 /** 0047 * Reference to the HTTP Request object 0048 * 0049 * @var Zend_Controller_Request_Http 0050 */ 0051 protected $_request; 0052 0053 /** 0054 * Reference to the HTTP Response object 0055 * 0056 * @var Zend_Controller_Response_Http 0057 */ 0058 protected $_response; 0059 0060 /** 0061 * Object that looks up user credentials for the Basic scheme 0062 * 0063 * @var Zend_Auth_Adapter_Http_Resolver_Interface 0064 */ 0065 protected $_basicResolver; 0066 0067 /** 0068 * Object that looks up user credentials for the Digest scheme 0069 * 0070 * @var Zend_Auth_Adapter_Http_Resolver_Interface 0071 */ 0072 protected $_digestResolver; 0073 0074 /** 0075 * List of authentication schemes supported by this class 0076 * 0077 * @var array 0078 */ 0079 protected $_supportedSchemes = array('basic', 'digest'); 0080 0081 /** 0082 * List of schemes this class will accept from the client 0083 * 0084 * @var array 0085 */ 0086 protected $_acceptSchemes; 0087 0088 /** 0089 * Space-delimited list of protected domains for Digest Auth 0090 * 0091 * @var string 0092 */ 0093 protected $_domains; 0094 0095 /** 0096 * The protection realm to use 0097 * 0098 * @var string 0099 */ 0100 protected $_realm; 0101 0102 /** 0103 * Nonce timeout period 0104 * 0105 * @var integer 0106 */ 0107 protected $_nonceTimeout; 0108 0109 /** 0110 * Whether to send the opaque value in the header. True by default 0111 * 0112 * @var boolean 0113 */ 0114 protected $_useOpaque; 0115 0116 /** 0117 * List of the supported digest algorithms. I want to support both MD5 and 0118 * MD5-sess, but MD5-sess won't make it into the first version. 0119 * 0120 * @var array 0121 */ 0122 protected $_supportedAlgos = array('MD5'); 0123 0124 /** 0125 * The actual algorithm to use. Defaults to MD5 0126 * 0127 * @var string 0128 */ 0129 protected $_algo; 0130 0131 /** 0132 * List of supported qop options. My intetion is to support both 'auth' and 0133 * 'auth-int', but 'auth-int' won't make it into the first version. 0134 * 0135 * @var array 0136 */ 0137 protected $_supportedQops = array('auth'); 0138 0139 /** 0140 * Whether or not to do Proxy Authentication instead of origin server 0141 * authentication (send 407's instead of 401's). Off by default. 0142 * 0143 * @var boolean 0144 */ 0145 protected $_imaProxy; 0146 0147 /** 0148 * Flag indicating the client is IE and didn't bother to return the opaque string 0149 * 0150 * @var boolean 0151 */ 0152 protected $_ieNoOpaque; 0153 0154 /** 0155 * Constructor 0156 * 0157 * @param array $config Configuration settings: 0158 * 'accept_schemes' => 'basic'|'digest'|'basic digest' 0159 * 'realm' => <string> 0160 * 'digest_domains' => <string> Space-delimited list of URIs 0161 * 'nonce_timeout' => <int> 0162 * 'use_opaque' => <bool> Whether to send the opaque value in the header 0163 * 'alogrithm' => <string> See $_supportedAlgos. Default: MD5 0164 * 'proxy_auth' => <bool> Whether to do authentication as a Proxy 0165 * @throws Zend_Auth_Adapter_Exception 0166 */ 0167 public function __construct(array $config) 0168 { 0169 if (!extension_loaded('hash')) { 0170 /** 0171 * @see Zend_Auth_Adapter_Exception 0172 */ 0173 // require_once 'Zend/Auth/Adapter/Exception.php'; 0174 throw new Zend_Auth_Adapter_Exception(__CLASS__ . ' requires the \'hash\' extension'); 0175 } 0176 0177 $this->_request = null; 0178 $this->_response = null; 0179 $this->_ieNoOpaque = false; 0180 0181 0182 if (empty($config['accept_schemes'])) { 0183 /** 0184 * @see Zend_Auth_Adapter_Exception 0185 */ 0186 // require_once 'Zend/Auth/Adapter/Exception.php'; 0187 throw new Zend_Auth_Adapter_Exception('Config key \'accept_schemes\' is required'); 0188 } 0189 0190 $schemes = explode(' ', $config['accept_schemes']); 0191 $this->_acceptSchemes = array_intersect($schemes, $this->_supportedSchemes); 0192 if (empty($this->_acceptSchemes)) { 0193 /** 0194 * @see Zend_Auth_Adapter_Exception 0195 */ 0196 // require_once 'Zend/Auth/Adapter/Exception.php'; 0197 throw new Zend_Auth_Adapter_Exception('No supported schemes given in \'accept_schemes\'. Valid values: ' 0198 . implode(', ', $this->_supportedSchemes)); 0199 } 0200 0201 // Double-quotes are used to delimit the realm string in the HTTP header, 0202 // and colons are field delimiters in the password file. 0203 if (empty($config['realm']) || 0204 !ctype_print($config['realm']) || 0205 strpos($config['realm'], ':') !== false || 0206 strpos($config['realm'], '"') !== false) { 0207 /** 0208 * @see Zend_Auth_Adapter_Exception 0209 */ 0210 // require_once 'Zend/Auth/Adapter/Exception.php'; 0211 throw new Zend_Auth_Adapter_Exception('Config key \'realm\' is required, and must contain only printable ' 0212 . 'characters, excluding quotation marks and colons'); 0213 } else { 0214 $this->_realm = $config['realm']; 0215 } 0216 0217 if (in_array('digest', $this->_acceptSchemes)) { 0218 if (empty($config['digest_domains']) || 0219 !ctype_print($config['digest_domains']) || 0220 strpos($config['digest_domains'], '"') !== false) { 0221 /** 0222 * @see Zend_Auth_Adapter_Exception 0223 */ 0224 // require_once 'Zend/Auth/Adapter/Exception.php'; 0225 throw new Zend_Auth_Adapter_Exception('Config key \'digest_domains\' is required, and must contain ' 0226 . 'only printable characters, excluding quotation marks'); 0227 } else { 0228 $this->_domains = $config['digest_domains']; 0229 } 0230 0231 if (empty($config['nonce_timeout']) || 0232 !is_numeric($config['nonce_timeout'])) { 0233 /** 0234 * @see Zend_Auth_Adapter_Exception 0235 */ 0236 // require_once 'Zend/Auth/Adapter/Exception.php'; 0237 throw new Zend_Auth_Adapter_Exception('Config key \'nonce_timeout\' is required, and must be an ' 0238 . 'integer'); 0239 } else { 0240 $this->_nonceTimeout = (int) $config['nonce_timeout']; 0241 } 0242 0243 // We use the opaque value unless explicitly told not to 0244 if (isset($config['use_opaque']) && false == (bool) $config['use_opaque']) { 0245 $this->_useOpaque = false; 0246 } else { 0247 $this->_useOpaque = true; 0248 } 0249 0250 if (isset($config['algorithm']) && in_array($config['algorithm'], $this->_supportedAlgos)) { 0251 $this->_algo = $config['algorithm']; 0252 } else { 0253 $this->_algo = 'MD5'; 0254 } 0255 } 0256 0257 // Don't be a proxy unless explicitly told to do so 0258 if (isset($config['proxy_auth']) && true == (bool) $config['proxy_auth']) { 0259 $this->_imaProxy = true; // I'm a Proxy 0260 } else { 0261 $this->_imaProxy = false; 0262 } 0263 } 0264 0265 /** 0266 * Setter for the _basicResolver property 0267 * 0268 * @param Zend_Auth_Adapter_Http_Resolver_Interface $resolver 0269 * @return Zend_Auth_Adapter_Http Provides a fluent interface 0270 */ 0271 public function setBasicResolver(Zend_Auth_Adapter_Http_Resolver_Interface $resolver) 0272 { 0273 $this->_basicResolver = $resolver; 0274 0275 return $this; 0276 } 0277 0278 /** 0279 * Getter for the _basicResolver property 0280 * 0281 * @return Zend_Auth_Adapter_Http_Resolver_Interface 0282 */ 0283 public function getBasicResolver() 0284 { 0285 return $this->_basicResolver; 0286 } 0287 0288 /** 0289 * Setter for the _digestResolver property 0290 * 0291 * @param Zend_Auth_Adapter_Http_Resolver_Interface $resolver 0292 * @return Zend_Auth_Adapter_Http Provides a fluent interface 0293 */ 0294 public function setDigestResolver(Zend_Auth_Adapter_Http_Resolver_Interface $resolver) 0295 { 0296 $this->_digestResolver = $resolver; 0297 0298 return $this; 0299 } 0300 0301 /** 0302 * Getter for the _digestResolver property 0303 * 0304 * @return Zend_Auth_Adapter_Http_Resolver_Interface 0305 */ 0306 public function getDigestResolver() 0307 { 0308 return $this->_digestResolver; 0309 } 0310 0311 /** 0312 * Setter for the Request object 0313 * 0314 * @param Zend_Controller_Request_Http $request 0315 * @return Zend_Auth_Adapter_Http Provides a fluent interface 0316 */ 0317 public function setRequest(Zend_Controller_Request_Http $request) 0318 { 0319 $this->_request = $request; 0320 0321 return $this; 0322 } 0323 0324 /** 0325 * Getter for the Request object 0326 * 0327 * @return Zend_Controller_Request_Http 0328 */ 0329 public function getRequest() 0330 { 0331 return $this->_request; 0332 } 0333 0334 /** 0335 * Setter for the Response object 0336 * 0337 * @param Zend_Controller_Response_Http $response 0338 * @return Zend_Auth_Adapter_Http Provides a fluent interface 0339 */ 0340 public function setResponse(Zend_Controller_Response_Http $response) 0341 { 0342 $this->_response = $response; 0343 0344 return $this; 0345 } 0346 0347 /** 0348 * Getter for the Response object 0349 * 0350 * @return Zend_Controller_Response_Http 0351 */ 0352 public function getResponse() 0353 { 0354 return $this->_response; 0355 } 0356 0357 /** 0358 * Authenticate 0359 * 0360 * @throws Zend_Auth_Adapter_Exception 0361 * @return Zend_Auth_Result 0362 */ 0363 public function authenticate() 0364 { 0365 if (empty($this->_request) || 0366 empty($this->_response)) { 0367 /** 0368 * @see Zend_Auth_Adapter_Exception 0369 */ 0370 // require_once 'Zend/Auth/Adapter/Exception.php'; 0371 throw new Zend_Auth_Adapter_Exception('Request and Response objects must be set before calling ' 0372 . 'authenticate()'); 0373 } 0374 0375 if ($this->_imaProxy) { 0376 $getHeader = 'Proxy-Authorization'; 0377 } else { 0378 $getHeader = 'Authorization'; 0379 } 0380 0381 $authHeader = $this->_request->getHeader($getHeader); 0382 if (!$authHeader) { 0383 return $this->_challengeClient(); 0384 } 0385 0386 list($clientScheme) = explode(' ', $authHeader); 0387 $clientScheme = strtolower($clientScheme); 0388 0389 // The server can issue multiple challenges, but the client should 0390 // answer with only the selected auth scheme. 0391 if (!in_array($clientScheme, $this->_supportedSchemes)) { 0392 $this->_response->setHttpResponseCode(400); 0393 return new Zend_Auth_Result( 0394 Zend_Auth_Result::FAILURE_UNCATEGORIZED, 0395 array(), 0396 array('Client requested an incorrect or unsupported authentication scheme') 0397 ); 0398 } 0399 0400 // client sent a scheme that is not the one required 0401 if (!in_array($clientScheme, $this->_acceptSchemes)) { 0402 // challenge again the client 0403 return $this->_challengeClient(); 0404 } 0405 0406 switch ($clientScheme) { 0407 case 'basic': 0408 $result = $this->_basicAuth($authHeader); 0409 break; 0410 case 'digest': 0411 $result = $this->_digestAuth($authHeader); 0412 break; 0413 default: 0414 /** 0415 * @see Zend_Auth_Adapter_Exception 0416 */ 0417 // require_once 'Zend/Auth/Adapter/Exception.php'; 0418 throw new Zend_Auth_Adapter_Exception('Unsupported authentication scheme'); 0419 } 0420 0421 return $result; 0422 } 0423 0424 /** 0425 * Challenge Client 0426 * 0427 * Sets a 401 or 407 Unauthorized response code, and creates the 0428 * appropriate Authenticate header(s) to prompt for credentials. 0429 * 0430 * @return Zend_Auth_Result Always returns a non-identity Auth result 0431 */ 0432 protected function _challengeClient() 0433 { 0434 if ($this->_imaProxy) { 0435 $statusCode = 407; 0436 $headerName = 'Proxy-Authenticate'; 0437 } else { 0438 $statusCode = 401; 0439 $headerName = 'WWW-Authenticate'; 0440 } 0441 0442 $this->_response->setHttpResponseCode($statusCode); 0443 0444 // Send a challenge in each acceptable authentication scheme 0445 if (in_array('basic', $this->_acceptSchemes)) { 0446 $this->_response->setHeader($headerName, $this->_basicHeader()); 0447 } 0448 if (in_array('digest', $this->_acceptSchemes)) { 0449 $this->_response->setHeader($headerName, $this->_digestHeader()); 0450 } 0451 return new Zend_Auth_Result( 0452 Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID, 0453 array(), 0454 array('Invalid or absent credentials; challenging client') 0455 ); 0456 } 0457 0458 /** 0459 * Basic Header 0460 * 0461 * Generates a Proxy- or WWW-Authenticate header value in the Basic 0462 * authentication scheme. 0463 * 0464 * @return string Authenticate header value 0465 */ 0466 protected function _basicHeader() 0467 { 0468 return 'Basic realm="' . $this->_realm . '"'; 0469 } 0470 0471 /** 0472 * Digest Header 0473 * 0474 * Generates a Proxy- or WWW-Authenticate header value in the Digest 0475 * authentication scheme. 0476 * 0477 * @return string Authenticate header value 0478 */ 0479 protected function _digestHeader() 0480 { 0481 $wwwauth = 'Digest realm="' . $this->_realm . '", ' 0482 . 'domain="' . $this->_domains . '", ' 0483 . 'nonce="' . $this->_calcNonce() . '", ' 0484 . ($this->_useOpaque ? 'opaque="' . $this->_calcOpaque() . '", ' : '') 0485 . 'algorithm="' . $this->_algo . '", ' 0486 . 'qop="' . implode(',', $this->_supportedQops) . '"'; 0487 0488 return $wwwauth; 0489 } 0490 0491 /** 0492 * Basic Authentication 0493 * 0494 * @param string $header Client's Authorization header 0495 * @throws Zend_Auth_Adapter_Exception 0496 * @return Zend_Auth_Result 0497 */ 0498 protected function _basicAuth($header) 0499 { 0500 if (empty($header)) { 0501 /** 0502 * @see Zend_Auth_Adapter_Exception 0503 */ 0504 // require_once 'Zend/Auth/Adapter/Exception.php'; 0505 throw new Zend_Auth_Adapter_Exception('The value of the client Authorization header is required'); 0506 } 0507 if (empty($this->_basicResolver)) { 0508 /** 0509 * @see Zend_Auth_Adapter_Exception 0510 */ 0511 // require_once 'Zend/Auth/Adapter/Exception.php'; 0512 throw new Zend_Auth_Adapter_Exception('A basicResolver object must be set before doing Basic ' 0513 . 'authentication'); 0514 } 0515 0516 // Decode the Authorization header 0517 $auth = substr($header, strlen('Basic ')); 0518 $auth = base64_decode($auth); 0519 if (!$auth) { 0520 /** 0521 * @see Zend_Auth_Adapter_Exception 0522 */ 0523 // require_once 'Zend/Auth/Adapter/Exception.php'; 0524 throw new Zend_Auth_Adapter_Exception('Unable to base64_decode Authorization header value'); 0525 } 0526 0527 // See ZF-1253. Validate the credentials the same way the digest 0528 // implementation does. If invalid credentials are detected, 0529 // re-challenge the client. 0530 if (!ctype_print($auth)) { 0531 return $this->_challengeClient(); 0532 } 0533 // Fix for ZF-1515: Now re-challenges on empty username or password 0534 $creds = array_filter(explode(':', $auth)); 0535 if (count($creds) != 2) { 0536 return $this->_challengeClient(); 0537 } 0538 0539 $password = $this->_basicResolver->resolve($creds[0], $this->_realm); 0540 if ($password && $this->_secureStringCompare($password, $creds[1])) { 0541 $identity = array('username'=>$creds[0], 'realm'=>$this->_realm); 0542 return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $identity); 0543 } else { 0544 return $this->_challengeClient(); 0545 } 0546 } 0547 0548 /** 0549 * Digest Authentication 0550 * 0551 * @param string $header Client's Authorization header 0552 * @throws Zend_Auth_Adapter_Exception 0553 * @return Zend_Auth_Result Valid auth result only on successful auth 0554 */ 0555 protected function _digestAuth($header) 0556 { 0557 if (empty($header)) { 0558 /** 0559 * @see Zend_Auth_Adapter_Exception 0560 */ 0561 // require_once 'Zend/Auth/Adapter/Exception.php'; 0562 throw new Zend_Auth_Adapter_Exception('The value of the client Authorization header is required'); 0563 } 0564 if (empty($this->_digestResolver)) { 0565 /** 0566 * @see Zend_Auth_Adapter_Exception 0567 */ 0568 // require_once 'Zend/Auth/Adapter/Exception.php'; 0569 throw new Zend_Auth_Adapter_Exception('A digestResolver object must be set before doing Digest authentication'); 0570 } 0571 0572 $data = $this->_parseDigestAuth($header); 0573 if ($data === false) { 0574 $this->_response->setHttpResponseCode(400); 0575 return new Zend_Auth_Result( 0576 Zend_Auth_Result::FAILURE_UNCATEGORIZED, 0577 array(), 0578 array('Invalid Authorization header format') 0579 ); 0580 } 0581 0582 // See ZF-1052. This code was a bit too unforgiving of invalid 0583 // usernames. Now, if the username is bad, we re-challenge the client. 0584 if ('::invalid::' == $data['username']) { 0585 return $this->_challengeClient(); 0586 } 0587 0588 // Verify that the client sent back the same nonce 0589 if ($this->_calcNonce() != $data['nonce']) { 0590 return $this->_challengeClient(); 0591 } 0592 // The opaque value is also required to match, but of course IE doesn't 0593 // play ball. 0594 if (!$this->_ieNoOpaque && $this->_calcOpaque() != $data['opaque']) { 0595 return $this->_challengeClient(); 0596 } 0597 0598 // Look up the user's password hash. If not found, deny access. 0599 // This makes no assumptions about how the password hash was 0600 // constructed beyond that it must have been built in such a way as 0601 // to be recreatable with the current settings of this object. 0602 $ha1 = $this->_digestResolver->resolve($data['username'], $data['realm']); 0603 if ($ha1 === false) { 0604 return $this->_challengeClient(); 0605 } 0606 0607 // If MD5-sess is used, a1 value is made of the user's password 0608 // hash with the server and client nonce appended, separated by 0609 // colons. 0610 if ($this->_algo == 'MD5-sess') { 0611 $ha1 = hash('md5', $ha1 . ':' . $data['nonce'] . ':' . $data['cnonce']); 0612 } 0613 0614 // Calculate h(a2). The value of this hash depends on the qop 0615 // option selected by the client and the supported hash functions 0616 switch ($data['qop']) { 0617 case 'auth': 0618 $a2 = $this->_request->getMethod() . ':' . $data['uri']; 0619 break; 0620 case 'auth-int': 0621 // Should be REQUEST_METHOD . ':' . uri . ':' . hash(entity-body), 0622 // but this isn't supported yet, so fall through to default case 0623 default: 0624 /** 0625 * @see Zend_Auth_Adapter_Exception 0626 */ 0627 // require_once 'Zend/Auth/Adapter/Exception.php'; 0628 throw new Zend_Auth_Adapter_Exception('Client requested an unsupported qop option'); 0629 } 0630 // Using hash() should make parameterizing the hash algorithm 0631 // easier 0632 $ha2 = hash('md5', $a2); 0633 0634 0635 // Calculate the server's version of the request-digest. This must 0636 // match $data['response']. See RFC 2617, section 3.2.2.1 0637 $message = $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $ha2; 0638 $digest = hash('md5', $ha1 . ':' . $message); 0639 0640 // If our digest matches the client's let them in, otherwise return 0641 // a 401 code and exit to prevent access to the protected resource. 0642 if ($this->_secureStringCompare($digest, $data['response'])) { 0643 $identity = array('username'=>$data['username'], 'realm'=>$data['realm']); 0644 return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $identity); 0645 } else { 0646 return $this->_challengeClient(); 0647 } 0648 } 0649 0650 /** 0651 * Calculate Nonce 0652 * 0653 * @return string The nonce value 0654 */ 0655 protected function _calcNonce() 0656 { 0657 // Once subtle consequence of this timeout calculation is that it 0658 // actually divides all of time into _nonceTimeout-sized sections, such 0659 // that the value of timeout is the point in time of the next 0660 // approaching "boundary" of a section. This allows the server to 0661 // consistently generate the same timeout (and hence the same nonce 0662 // value) across requests, but only as long as one of those 0663 // "boundaries" is not crossed between requests. If that happens, the 0664 // nonce will change on its own, and effectively log the user out. This 0665 // would be surprising if the user just logged in. 0666 $timeout = ceil(time() / $this->_nonceTimeout) * $this->_nonceTimeout; 0667 0668 $nonce = hash('md5', $timeout . ':' . $this->_request->getServer('HTTP_USER_AGENT') . ':' . __CLASS__); 0669 return $nonce; 0670 } 0671 0672 /** 0673 * Calculate Opaque 0674 * 0675 * The opaque string can be anything; the client must return it exactly as 0676 * it was sent. It may be useful to store data in this string in some 0677 * applications. Ideally, a new value for this would be generated each time 0678 * a WWW-Authenticate header is sent (in order to reduce predictability), 0679 * but we would have to be able to create the same exact value across at 0680 * least two separate requests from the same client. 0681 * 0682 * @return string The opaque value 0683 */ 0684 protected function _calcOpaque() 0685 { 0686 return hash('md5', 'Opaque Data:' . __CLASS__); 0687 } 0688 0689 /** 0690 * Parse Digest Authorization header 0691 * 0692 * @param string $header Client's Authorization: HTTP header 0693 * @return array|false Data elements from header, or false if any part of 0694 * the header is invalid 0695 */ 0696 protected function _parseDigestAuth($header) 0697 { 0698 $temp = null; 0699 $data = array(); 0700 0701 // See ZF-1052. Detect invalid usernames instead of just returning a 0702 // 400 code. 0703 $ret = preg_match('/username="([^"]+)"/', $header, $temp); 0704 if (!$ret || empty($temp[1]) 0705 || !ctype_print($temp[1]) 0706 || strpos($temp[1], ':') !== false) { 0707 $data['username'] = '::invalid::'; 0708 } else { 0709 $data['username'] = $temp[1]; 0710 } 0711 $temp = null; 0712 0713 $ret = preg_match('/realm="([^"]+)"/', $header, $temp); 0714 if (!$ret || empty($temp[1])) { 0715 return false; 0716 } 0717 if (!ctype_print($temp[1]) || strpos($temp[1], ':') !== false) { 0718 return false; 0719 } else { 0720 $data['realm'] = $temp[1]; 0721 } 0722 $temp = null; 0723 0724 $ret = preg_match('/nonce="([^"]+)"/', $header, $temp); 0725 if (!$ret || empty($temp[1])) { 0726 return false; 0727 } 0728 if (!ctype_xdigit($temp[1])) { 0729 return false; 0730 } else { 0731 $data['nonce'] = $temp[1]; 0732 } 0733 $temp = null; 0734 0735 $ret = preg_match('/uri="([^"]+)"/', $header, $temp); 0736 if (!$ret || empty($temp[1])) { 0737 return false; 0738 } 0739 // Section 3.2.2.5 in RFC 2617 says the authenticating server must 0740 // verify that the URI field in the Authorization header is for the 0741 // same resource requested in the Request Line. 0742 $rUri = @parse_url($this->_request->getRequestUri()); 0743 $cUri = @parse_url($temp[1]); 0744 if (false === $rUri || false === $cUri) { 0745 return false; 0746 } else { 0747 // Make sure the path portion of both URIs is the same 0748 if ($rUri['path'] != $cUri['path']) { 0749 return false; 0750 } 0751 // Section 3.2.2.5 seems to suggest that the value of the URI 0752 // Authorization field should be made into an absolute URI if the 0753 // Request URI is absolute, but it's vague, and that's a bunch of 0754 // code I don't want to write right now. 0755 $data['uri'] = $temp[1]; 0756 } 0757 $temp = null; 0758 0759 $ret = preg_match('/response="([^"]+)"/', $header, $temp); 0760 if (!$ret || empty($temp[1])) { 0761 return false; 0762 } 0763 if (32 != strlen($temp[1]) || !ctype_xdigit($temp[1])) { 0764 return false; 0765 } else { 0766 $data['response'] = $temp[1]; 0767 } 0768 $temp = null; 0769 0770 // The spec says this should default to MD5 if omitted. OK, so how does 0771 // that square with the algo we send out in the WWW-Authenticate header, 0772 // if it can easily be overridden by the client? 0773 $ret = preg_match('/algorithm="?(' . $this->_algo . ')"?/', $header, $temp); 0774 if ($ret && !empty($temp[1]) 0775 && in_array($temp[1], $this->_supportedAlgos)) { 0776 $data['algorithm'] = $temp[1]; 0777 } else { 0778 $data['algorithm'] = 'MD5'; // = $this->_algo; ? 0779 } 0780 $temp = null; 0781 0782 // Not optional in this implementation 0783 $ret = preg_match('/cnonce="([^"]+)"/', $header, $temp); 0784 if (!$ret || empty($temp[1])) { 0785 return false; 0786 } 0787 if (!ctype_print($temp[1])) { 0788 return false; 0789 } else { 0790 $data['cnonce'] = $temp[1]; 0791 } 0792 $temp = null; 0793 0794 // If the server sent an opaque value, the client must send it back 0795 if ($this->_useOpaque) { 0796 $ret = preg_match('/opaque="([^"]+)"/', $header, $temp); 0797 if (!$ret || empty($temp[1])) { 0798 0799 // Big surprise: IE isn't RFC 2617-compliant. 0800 if (false !== strpos($this->_request->getHeader('User-Agent'), 'MSIE')) { 0801 $temp[1] = ''; 0802 $this->_ieNoOpaque = true; 0803 } else { 0804 return false; 0805 } 0806 } 0807 // This implementation only sends MD5 hex strings in the opaque value 0808 if (!$this->_ieNoOpaque && 0809 (32 != strlen($temp[1]) || !ctype_xdigit($temp[1]))) { 0810 return false; 0811 } else { 0812 $data['opaque'] = $temp[1]; 0813 } 0814 $temp = null; 0815 } 0816 0817 // Not optional in this implementation, but must be one of the supported 0818 // qop types 0819 $ret = preg_match('/qop="?(' . implode('|', $this->_supportedQops) . ')"?/', $header, $temp); 0820 if (!$ret || empty($temp[1])) { 0821 return false; 0822 } 0823 if (!in_array($temp[1], $this->_supportedQops)) { 0824 return false; 0825 } else { 0826 $data['qop'] = $temp[1]; 0827 } 0828 $temp = null; 0829 0830 // Not optional in this implementation. The spec says this value 0831 // shouldn't be a quoted string, but apparently some implementations 0832 // quote it anyway. See ZF-1544. 0833 $ret = preg_match('/nc="?([0-9A-Fa-f]{8})"?/', $header, $temp); 0834 if (!$ret || empty($temp[1])) { 0835 return false; 0836 } 0837 if (8 != strlen($temp[1]) || !ctype_xdigit($temp[1])) { 0838 return false; 0839 } else { 0840 $data['nc'] = $temp[1]; 0841 } 0842 $temp = null; 0843 0844 return $data; 0845 } 0846 0847 /** 0848 * Securely compare two strings for equality while avoided C level memcmp() 0849 * optimisations capable of leaking timing information useful to an attacker 0850 * attempting to iteratively guess the unknown string (e.g. password) being 0851 * compared against. 0852 * 0853 * @param string $a 0854 * @param string $b 0855 * @return bool 0856 */ 0857 protected function _secureStringCompare($a, $b) 0858 { 0859 if (strlen($a) !== strlen($b)) { 0860 return false; 0861 } 0862 $result = 0; 0863 for ($i = 0; $i < strlen($a); $i++) { 0864 $result |= ord($a[$i]) ^ ord($b[$i]); 0865 } 0866 return $result == 0; 0867 } 0868 }