web/lib/Zend/Auth/Adapter/Http.php
changeset 64 162c1de6545a
parent 19 1c2f13fd785c
child 68 ecaf28ffe26e
equal deleted inserted replaced
63:5b37998e522e 64:162c1de6545a
       
     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 }