DigestAuthenticate.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  13. */
  14. App::uses('BasicAuthenticate', 'Controller/Component/Auth');
  15. /**
  16. * Digest Authentication adapter for AuthComponent.
  17. *
  18. * Provides Digest HTTP authentication support for AuthComponent. Unlike most AuthComponent adapters,
  19. * DigestAuthenticate requires a special password hash that conforms to RFC2617. You can create this
  20. * password using `DigestAuthenticate::password()`. If you wish to use digest authentication alongside other
  21. * authentication methods, its recommended that you store the digest authentication separately.
  22. *
  23. * Clients using Digest Authentication must support cookies. Since AuthComponent identifies users based
  24. * on Session contents, clients without support for cookies will not function properly.
  25. *
  26. * ### Using Digest auth
  27. *
  28. * In your controller's components array, add auth + the required settings.
  29. * {{{
  30. * public $components = array(
  31. * 'Auth' => array(
  32. * 'authenticate' => array('Digest')
  33. * )
  34. * );
  35. * }}}
  36. *
  37. * In your login function just call `$this->Auth->login()` without any checks for POST data. This
  38. * will send the authentication headers, and trigger the login dialog in the browser/client.
  39. *
  40. * ### Generating passwords compatible with Digest authentication.
  41. *
  42. * Due to the Digest authentication specification, digest auth requires a special password value. You
  43. * can generate this password using `DigestAuthenticate::password()`
  44. *
  45. * `$digestPass = DigestAuthenticate::password($username, env('SERVER_NAME'), $password);`
  46. *
  47. * Its recommended that you store this digest auth only password separate from password hashes used for other
  48. * login methods. For example `User.digest_pass` could be used for a digest password, while `User.password` would
  49. * store the password hash for use with other methods like Basic or Form.
  50. *
  51. * @package Cake.Controller.Component.Auth
  52. * @since 2.0
  53. */
  54. class DigestAuthenticate extends BasicAuthenticate {
  55. /**
  56. * Settings for this object.
  57. *
  58. * - `fields` The fields to use to identify a user by.
  59. * - `userModel` The model name of the User, defaults to User.
  60. * - `scope` Additional conditions to use when looking up and authenticating users,
  61. * i.e. `array('User.is_active' => 1).`
  62. * - `recursive` The value of the recursive key passed to find(). Defaults to 0.
  63. * - `contain` Extra models to contain and store in session.
  64. * - `realm` The realm authentication is for, Defaults to the servername.
  65. * - `nonce` A nonce used for authentication. Defaults to `uniqid()`.
  66. * - `qop` Defaults to auth, no other values are supported at this time.
  67. * - `opaque` A string that must be returned unchanged by clients.
  68. * Defaults to `md5($settings['realm'])`
  69. *
  70. * @var array
  71. */
  72. public $settings = array(
  73. 'fields' => array(
  74. 'username' => 'username',
  75. 'password' => 'password'
  76. ),
  77. 'userModel' => 'User',
  78. 'scope' => array(),
  79. 'recursive' => 0,
  80. 'contain' => null,
  81. 'realm' => '',
  82. 'qop' => 'auth',
  83. 'nonce' => '',
  84. 'opaque' => '',
  85. 'passwordHasher' => 'Simple',
  86. );
  87. /**
  88. * Constructor, completes configuration for digest authentication.
  89. *
  90. * @param ComponentCollection $collection The Component collection used on this request.
  91. * @param array $settings An array of settings.
  92. */
  93. public function __construct(ComponentCollection $collection, $settings) {
  94. parent::__construct($collection, $settings);
  95. if (empty($this->settings['nonce'])) {
  96. $this->settings['nonce'] = uniqid('');
  97. }
  98. if (empty($this->settings['opaque'])) {
  99. $this->settings['opaque'] = md5($this->settings['realm']);
  100. }
  101. }
  102. /**
  103. * Get a user based on information in the request. Used by cookie-less auth for stateless clients.
  104. *
  105. * @param CakeRequest $request Request object.
  106. * @return mixed Either false or an array of user information
  107. */
  108. public function getUser(CakeRequest $request) {
  109. $digest = $this->_getDigest();
  110. if (empty($digest)) {
  111. return false;
  112. }
  113. list(, $model) = pluginSplit($this->settings['userModel']);
  114. $user = $this->_findUser(array(
  115. $model . '.' . $this->settings['fields']['username'] => $digest['username']
  116. ));
  117. if (empty($user)) {
  118. return false;
  119. }
  120. $password = $user[$this->settings['fields']['password']];
  121. unset($user[$this->settings['fields']['password']]);
  122. if ($digest['response'] === $this->generateResponseHash($digest, $password)) {
  123. return $user;
  124. }
  125. return false;
  126. }
  127. /**
  128. * Gets the digest headers from the request/environment.
  129. *
  130. * @return array Array of digest information.
  131. */
  132. protected function _getDigest() {
  133. $digest = env('PHP_AUTH_DIGEST');
  134. if (empty($digest) && function_exists('apache_request_headers')) {
  135. $headers = apache_request_headers();
  136. if (!empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) === 'Digest ') {
  137. $digest = substr($headers['Authorization'], 7);
  138. }
  139. }
  140. if (empty($digest)) {
  141. return false;
  142. }
  143. return $this->parseAuthData($digest);
  144. }
  145. /**
  146. * Parse the digest authentication headers and split them up.
  147. *
  148. * @param string $digest The raw digest authentication headers.
  149. * @return array An array of digest authentication headers
  150. */
  151. public function parseAuthData($digest) {
  152. if (substr($digest, 0, 7) === 'Digest ') {
  153. $digest = substr($digest, 7);
  154. }
  155. $keys = $match = array();
  156. $req = array('nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1);
  157. preg_match_all('/(\w+)=([\'"]?)([a-zA-Z0-9\:\#\%@=.\/_-]+)\2/', $digest, $match, PREG_SET_ORDER);
  158. foreach ($match as $i) {
  159. $keys[$i[1]] = $i[3];
  160. unset($req[$i[1]]);
  161. }
  162. if (empty($req)) {
  163. return $keys;
  164. }
  165. return null;
  166. }
  167. /**
  168. * Generate the response hash for a given digest array.
  169. *
  170. * @param array $digest Digest information containing data from DigestAuthenticate::parseAuthData().
  171. * @param string $password The digest hash password generated with DigestAuthenticate::password()
  172. * @return string Response hash
  173. */
  174. public function generateResponseHash($digest, $password) {
  175. return md5(
  176. $password .
  177. ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' .
  178. md5(env('REQUEST_METHOD') . ':' . $digest['uri'])
  179. );
  180. }
  181. /**
  182. * Creates an auth digest password hash to store
  183. *
  184. * @param string $username The username to use in the digest hash.
  185. * @param string $password The unhashed password to make a digest hash for.
  186. * @param string $realm The realm the password is for.
  187. * @return string the hashed password that can later be used with Digest authentication.
  188. */
  189. public static function password($username, $password, $realm) {
  190. return md5($username . ':' . $realm . ':' . $password);
  191. }
  192. /**
  193. * Generate the login headers
  194. *
  195. * @return string Headers for logging in.
  196. */
  197. public function loginHeaders() {
  198. $options = array(
  199. 'realm' => $this->settings['realm'],
  200. 'qop' => $this->settings['qop'],
  201. 'nonce' => $this->settings['nonce'],
  202. 'opaque' => $this->settings['opaque']
  203. );
  204. $opts = array();
  205. foreach ($options as $k => $v) {
  206. $opts[] = sprintf('%s="%s"', $k, $v);
  207. }
  208. return 'WWW-Authenticate: Digest ' . implode(',', $opts);
  209. }
  210. }