DigestAuthenticate.php 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 2.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Auth;
  16. use Cake\Auth\BasicAuthenticate;
  17. use Cake\Controller\ComponentRegistry;
  18. use Cake\Network\Request;
  19. /**
  20. * Digest Authentication adapter for AuthComponent.
  21. *
  22. * Provides Digest HTTP authentication support for AuthComponent.
  23. *
  24. * ### Using Digest auth
  25. *
  26. * In your controller's components array, add auth + the required config
  27. * ```
  28. * public $components = [
  29. * 'Auth' => [
  30. * 'authenticate' => ['Digest']
  31. * ]
  32. * ];
  33. * ```
  34. *
  35. * You should also set `AuthComponent::$sessionKey = false;` in your AppController's
  36. * beforeFilter() to prevent CakePHP from sending a session cookie to the client.
  37. *
  38. * Since HTTP Digest Authentication is stateless you don't need a login() action
  39. * in your controller. The user credentials will be checked on each request. If
  40. * valid credentials are not provided, required authentication headers will be sent
  41. * by this authentication provider which triggers the login dialog in the browser/client.
  42. *
  43. * You may also want to use `$this->Auth->unauthorizedRedirect = false;`.
  44. * This causes AuthComponent to throw a ForbiddenException exception instead of
  45. * redirecting to another page.
  46. *
  47. * ### Generating passwords compatible with Digest authentication.
  48. *
  49. * DigestAuthenticate requires a special password hash that conforms to RFC2617.
  50. * You can generate this password using `DigestAuthenticate::password()`
  51. *
  52. * ```
  53. * $digestPass = DigestAuthenticate::password($username, $password, env('SERVER_NAME'));
  54. * ```
  55. *
  56. * If you wish to use digest authentication alongside other authentication methods,
  57. * it's recommended that you store the digest authentication separately. For
  58. * example `User.digest_pass` could be used for a digest password, while
  59. * `User.password` would store the password hash for use with other methods like
  60. * Basic or Form.
  61. */
  62. class DigestAuthenticate extends BasicAuthenticate
  63. {
  64. /**
  65. * Constructor
  66. *
  67. * Besides the keys specified in BaseAuthenticate::$_defaultConfig,
  68. * DigestAuthenticate uses the following extra keys:
  69. *
  70. * - `realm` The realm authentication is for, Defaults to the servername.
  71. * - `nonce` A nonce used for authentication. Defaults to `uniqid()`.
  72. * - `qop` Defaults to 'auth', no other values are supported at this time.
  73. * - `opaque` A string that must be returned unchanged by clients.
  74. * Defaults to `md5($config['realm'])`
  75. *
  76. * @param \Cake\Controller\ComponentRegistry $registry The Component registry
  77. * used on this request.
  78. * @param array $config Array of config to use.
  79. */
  80. public function __construct(ComponentRegistry $registry, array $config = [])
  81. {
  82. $this->_registry = $registry;
  83. $this->config([
  84. 'realm' => null,
  85. 'qop' => 'auth',
  86. 'nonce' => uniqid(''),
  87. 'opaque' => null,
  88. ]);
  89. $this->config($config);
  90. }
  91. /**
  92. * Get a user based on information in the request. Used by cookie-less auth for stateless clients.
  93. *
  94. * @param \Cake\Network\Request $request Request object.
  95. * @return mixed Either false or an array of user information
  96. */
  97. public function getUser(Request $request)
  98. {
  99. $digest = $this->_getDigest($request);
  100. if (empty($digest)) {
  101. return false;
  102. }
  103. $user = $this->_findUser($digest['username']);
  104. if (empty($user)) {
  105. return false;
  106. }
  107. $field = $this->_config['fields']['password'];
  108. $password = $user[$field];
  109. unset($user[$field]);
  110. $hash = $this->generateResponseHash($digest, $password, $request->env('ORIGINAL_REQUEST_METHOD'));
  111. if ($digest['response'] === $hash) {
  112. return $user;
  113. }
  114. return false;
  115. }
  116. /**
  117. * Gets the digest headers from the request/environment.
  118. *
  119. * @param \Cake\Network\Request $request Request object.
  120. * @return array Array of digest information.
  121. */
  122. protected function _getDigest(Request $request)
  123. {
  124. $digest = $request->env('PHP_AUTH_DIGEST');
  125. if (empty($digest) && function_exists('apache_request_headers')) {
  126. $headers = apache_request_headers();
  127. if (!empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) === 'Digest ') {
  128. $digest = substr($headers['Authorization'], 7);
  129. }
  130. }
  131. if (empty($digest)) {
  132. return false;
  133. }
  134. return $this->parseAuthData($digest);
  135. }
  136. /**
  137. * Parse the digest authentication headers and split them up.
  138. *
  139. * @param string $digest The raw digest authentication headers.
  140. * @return array|null An array of digest authentication headers
  141. */
  142. public function parseAuthData($digest)
  143. {
  144. if (substr($digest, 0, 7) === 'Digest ') {
  145. $digest = substr($digest, 7);
  146. }
  147. $keys = $match = [];
  148. $req = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1];
  149. preg_match_all('/(\w+)=([\'"]?)([a-zA-Z0-9\:\#\%\?\&@=\.\/_-]+)\2/', $digest, $match, PREG_SET_ORDER);
  150. foreach ($match as $i) {
  151. $keys[$i[1]] = $i[3];
  152. unset($req[$i[1]]);
  153. }
  154. if (empty($req)) {
  155. return $keys;
  156. }
  157. return null;
  158. }
  159. /**
  160. * Generate the response hash for a given digest array.
  161. *
  162. * @param array $digest Digest information containing data from DigestAuthenticate::parseAuthData().
  163. * @param string $password The digest hash password generated with DigestAuthenticate::password()
  164. * @param string $method Request method
  165. * @return string Response hash
  166. */
  167. public function generateResponseHash($digest, $password, $method)
  168. {
  169. return md5(
  170. $password .
  171. ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' .
  172. md5($method . ':' . $digest['uri'])
  173. );
  174. }
  175. /**
  176. * Creates an auth digest password hash to store
  177. *
  178. * @param string $username The username to use in the digest hash.
  179. * @param string $password The unhashed password to make a digest hash for.
  180. * @param string $realm The realm the password is for.
  181. * @return string the hashed password that can later be used with Digest authentication.
  182. */
  183. public static function password($username, $password, $realm)
  184. {
  185. return md5($username . ':' . $realm . ':' . $password);
  186. }
  187. /**
  188. * Generate the login headers
  189. *
  190. * @param \Cake\Network\Request $request Request object.
  191. * @return string Headers for logging in.
  192. */
  193. public function loginHeaders(Request $request)
  194. {
  195. $realm = $this->_config['realm'] ?: $request->env('SERVER_NAME');
  196. $options = [
  197. 'realm' => $realm,
  198. 'qop' => $this->_config['qop'],
  199. 'nonce' => $this->_config['nonce'],
  200. 'opaque' => $this->_config['opaque'] ?: md5($realm)
  201. ];
  202. $opts = [];
  203. foreach ($options as $k => $v) {
  204. $opts[] = sprintf('%s="%s"', $k, $v);
  205. }
  206. return 'WWW-Authenticate: Digest ' . implode(',', $opts);
  207. }
  208. }