php digest authentication

November 16th, 2006 by admin

some php scripts (like oscommerce) do not have an user authentication.
therefor i google a little bit and found a nice small script from Paul James (thank you paul).
this script does an http digest authentication. this digest should fit all your needs for security and can replace the silly .htaccess.
i optimized the php class a little bit and make it much easier to use.
also i add the "stale" value wich is very important for the usability.
here is how to use it.

add this to your unprotected script (just at the top)

PHP:
  1. require('authentication.php');
  2. $auth = new HTTPDigest();
  3. $auth->baseURL = '/';
  4. $auth->nonceLife = 60;
  5. $auth->opaque = 'give me a random string';
  6. $auth->privateKey = 'thIS is my privat3 k3Y -> change me';
  7. $auth->realm = 'please login to '.$_SERVER['SERVER_NAME'];
  8.  
  9. //add the user user/pass
  10. $auth->passwordsHashed = false;
  11. $auth->add_user('user','pass');
  12. //or (it is mostly the same, but short)
  13. //$auth->add_user('user','pass',true);
  14.  
  15. //an better way is the hashed pass
  16. // you get the hashed pass with echo $auth->crypt_pass('user','pass');
  17. // it should look like this
  18. //$auth->add_user('user','9d1414171dff4c587870eac7da3fb929');
  19.  
  20. //thats it and of cause you can add more than one user
  21. //$auth->add_user('user1','pass1');
  22. //$auth->add_user('user2','pass2');
  23.  
  24. if (!$auth->authenticate())
  25. $auth->error();
  26. echo('Logged in as '.$auth->logged_in_user);

authentication.php:

PHP:
  1. /*
  2. orginal script @ http://www.peej.co.uk/projects/phphttpdigest.html
  3. Copyright (c) 2005 Paul James
  4. All rights reserved.
  5. Redistribution and use in source and binary forms, with or without
  6. modification, are permitted provided that the following conditions
  7. are met:
  8. 1. Redistributions of source code must retain the above copyright
  9. notice, this list of conditions and the following disclaimer.
  10. 2. Redistributions in binary form must reproduce the above copyright
  11. notice, this list of conditions and the following disclaimer in the
  12. documentation and/or other materials provided with the distribution.
  13. 3. Neither the name of the Paul James nor the names of its contributors
  14. may be used to endorse or promote products derived from this software
  15. without specific prior written permission.
  16. THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
  17. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  18. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  19. ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
  20. FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  21. DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  22. OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  23. HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  24. LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  25. OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  26. SUCH DAMAGE.
  27. */
  28.  
  29. /** HTTP Digest authentication class modified by macosbrain*/
  30. class HTTPDigest
  31. {
  32. /** The Digest opaque value (any string will do, never sent in plain text over the wire).
  33. * @var str
  34. */
  35. var $opaque = 'enter a random string';
  36. /** The authentication realm name.
  37. * @var str
  38. */
  39. var $realm = 'please login';
  40. /**
  41. * flag to indicate the nonce was stale.
  42. *
  43. * @var bool
  44. */
  45. var $stale = false;
  46. /** The base URL of the application, auth data will be used for all resources under this URL.
  47. * @var str
  48. */
  49. var $baseURL = '/';
  50. /** Are passwords stored as an a1 hash (username:realm:password) rather than plain text.
  51. * @var str
  52. */
  53. var $passwordsHashed = true;
  54. /** The private key.
  55. * @var str
  56. */
  57. var $privateKey = 'please choose one';
  58. /** The life of the nonce value in seconds
  59. * @var int
  60. */
  61. var $nonceLife = 30;// the $stale option allows us to set the $nonceLife very low
  62.  
  63. function HTTPDigest()
  64. {
  65. $this->unique = $_SERVER['SERVER_SOFTWARE'].$_SERVER['HTTP_HOST'].$_SERVER['SERVER_NAME'];//not a static value
  66. }
  67.  
  68. function error()
  69. {
  70. header('HTTP/1.1 401 Unauthorized');
  71. die('User authentication is required. You must enter a valid login ID and password to access this resource or have to disable your proxy-server.');
  72. }
  73. /** Send HTTP Auth header */
  74. function send()
  75. {
  76. $str = 'WWW-Authenticate: Digest '.
  77. 'realm="'.$this->realm.'", '.
  78. 'domain="'.$this->baseURL.'", '.
  79. 'qop=auth, '.
  80. 'algorithm=MD5, '.
  81. 'nonce="'.$this->getNonce().'", '.
  82. 'opaque="'.$this->getOpaque().'"';
  83. if ($this->stale) {
  84. $str .= ', stale=true';
  85. }
  86. header($str);
  87. header('HTTP/1.0 401 Unauthorized');
  88. exit();
  89. }
  90. /** Get the HTTP Auth header
  91. * @return str
  92. */
  93. function add_user($username,$password,$hash_it=false)
  94. {
  95. if ($hash_it==true)
  96. $a1 = $this->crypt_pass($username,$password);
  97. else
  98. $a1 = $password;
  99. $this->user[$username] = $a1;
  100. }
  101.  
  102. function crypt_pass($user,$pass)
  103. {
  104. return md5($user.':'.$this->getRealm().':'.$pass);
  105. }
  106.  
  107. function getAuthHeader()
  108. {
  109. if (isset($_SERVER['Authorization']))
  110. {
  111. return $_SERVER['Authorization'];
  112. }
  113. elseif (function_exists('apache_request_headers'))
  114. {
  115. if (isset($headers['Authorization']))
  116. return $headers['Authorization'];
  117. }
  118. return NULL;
  119. }
  120. /** Authenticate the user and return username on success.
  121. * @param str[] users Array of username/password pairs
  122. * @return str
  123. */
  124. function authenticate()
  125. {
  126. $authorization = $this->getAuthHeader();
  127. if (!$authorization)
  128. {
  129. $this->send();
  130. //die('HTTP Digest headers not being passed to PHP by the server, unable to authenticate user');
  131. }
  132. if (substr($authorization, 0, 5) == 'Basic')
  133. die('You are trying to use HTTP Basic authentication but I am expecting HTTP Digest');
  134. if (preg_match('/username="([^"]+)"/', $authorization, $username) && preg_match('/nonce="([^"]+)"/', $authorization, $nonce) && preg_match('/response="([^"]+)"/', $authorization, $response) && preg_match('/opaque="([^"]+)"/', $authorization, $opaque) && preg_match('/uri="([^"]+)"/', $authorization, $uri))
  135. {
  136. $username = $username[1];
  137. $requestURI = $_SERVER['REQUEST_URI'];
  138. if (strpos($requestURI, '?') !== FALSE)
  139. { // hack for IE which does not pass querystring in URI element of Digest string or in response hash
  140. $requestURI = substr($requestURI, 0, strlen($uri[1]));
  141. }
  142. if ( isset($this->user[$username]) && $opaque[1] == $this->getOpaque() && $uri[1] == $requestURI)
  143. {
  144. if($nonce[1] != $this->getNonce())
  145. {
  146. $this->stale = true;
  147. $this->send();
  148. }
  149. $passphrase = $this->user[$username];
  150. if ($this->passwordsHashed)
  151. $a1 = $passphrase;
  152. else
  153. $a1 = $this->crypt_pass($username,$passphrase);
  154.  
  155. $a2 = md5($_SERVER['REQUEST_METHOD'].':'.$requestURI);
  156. if (preg_match('/qop="?([^,\s"]+)/', $authorization, $qop) && preg_match('/nc=([^,\s"]+)/', $authorization, $nc) && preg_match('/cnonce="([^"]+)"/', $authorization, $cnonce))
  157. $expectedResponse = md5($a1.':'.$nonce[1].':'.$nc[1].':'.$cnonce[1].':'.$qop[1].':'.$a2);
  158. else
  159. $expectedResponse = md5($a1.':'.$nonce[1].':'.$a2);
  160. if ($response[1] == $expectedResponse)
  161. {
  162. $this->logged_in_user = $username;
  163. return TRUE;
  164. }
  165. }
  166. else
  167. $this->send();
  168. }
  169. return NULL;
  170. }
  171. /** Get nonce value for HTTP Digest.
  172. * @return str
  173. */
  174. function getNonce() {
  175. $time = ceil(time() / $this->nonceLife) * $this->nonceLife;
  176. $nonce = date('Y-m-d H:i', $time).':'.$_SERVER['REMOTE_ADDR'].':'.$this->privateKey.':'.$_SERVER['HTTP_USER_AGENT'].$this->unique;
  177. return md5($nonce);
  178. }
  179. /** Get opaque value for HTTP Digest.
  180. * @return str
  181. */
  182. function getOpaque()
  183. {
  184. return md5($this->opaque.$this->unique);
  185. }
  186. /** Get realm for HTTP Digest taking PHP safe mode into account.
  187. * @return str
  188. */
  189. function getRealm()
  190. {
  191. if (ini_get('safe_mode')) {
  192. return $this->realm.'-'.getmyuid();
  193. } else {
  194. return $this->realm;
  195. }
  196. }
  197. }

Posted in coding, php |

5 Responses

  1. Al Says:

    Did you ever get it to work with safari or opera?

    Thanks..

  2. admin Says:

    i know that it works with the most common browser(ie and firefox). with a small bugfix it will also work with the opera browser.

    i don’t got a mac right here so i can not test it with the safari browser.

  3. David Hendry Says:

    Hi the script works fine with ie7 but not ie6. I think this is because the username and password and not passed in the xmlHTTPRequest which remains undefined when using ie6 resulting in the $auth error condition. Any ideas?

    Thanks

    David

  4. max Says:

    hi

    I was trying to get digest authentication working on lighttpd server while processing response in javascript. But whenever server replies with 401 browser pops up login box which i dont want. Is there any way that i can get nonce & realm from server & pass it to javascript instead of browser….

  5. abhi Says:

    hi admin,

    can you let me know what the fix that will make it work with Opera? I am looking for a solution for this problem on opera.

Leave a Comment

Please note: Comment moderation is enabled and may delay your comment. There is no need to resubmit your comment.