1: <?php
2: /**
3: * PHP 5
4: *
5: * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
6: * Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
7: *
8: * Licensed under The MIT License
9: * Redistributions of files must retain the above copyright notice.
10: *
11: * @copyright Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
12: * @link http://cakephp.org CakePHP(tm) Project
13: * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
14: */
15:
16: App::uses('BaseAuthenticate', 'Controller/Component/Auth');
17:
18: /**
19: * Digest Authentication adapter for AuthComponent.
20: *
21: * Provides Digest HTTP authentication support for AuthComponent. Unlike most AuthComponent adapters,
22: * DigestAuthenticate requires a special password hash that conforms to RFC2617. You can create this
23: * password using `DigestAuthenticate::password()`. If you wish to use digest authentication alongside other
24: * authentication methods, its recommended that you store the digest authentication separately.
25: *
26: * Clients using Digest Authentication must support cookies. Since AuthComponent identifies users based
27: * on Session contents, clients without support for cookies will not function properly.
28: *
29: * ### Using Digest auth
30: *
31: * In your controller's components array, add auth + the required settings.
32: * {{{
33: * public $components = array(
34: * 'Auth' => array(
35: * 'authenticate' => array('Digest')
36: * )
37: * );
38: * }}}
39: *
40: * In your login function just call `$this->Auth->login()` without any checks for POST data. This
41: * will send the authentication headers, and trigger the login dialog in the browser/client.
42: *
43: * ### Generating passwords compatible with Digest authentication.
44: *
45: * Due to the Digest authentication specification, digest auth requires a special password value. You
46: * can generate this password using `DigestAuthenticate::password()`
47: *
48: * `$digestPass = DigestAuthenticate::password($username, env('SERVER_NAME'), $password);`
49: *
50: * Its recommended that you store this digest auth only password separate from password hashes used for other
51: * login methods. For example `User.digest_pass` could be used for a digest password, while `User.password` would
52: * store the password hash for use with other methods like Basic or Form.
53: *
54: * @package Cake.Controller.Component.Auth
55: * @since 2.0
56: */
57: class DigestAuthenticate extends BaseAuthenticate {
58: /**
59: * Settings for this object.
60: *
61: * - `fields` The fields to use to identify a user by.
62: * - `userModel` The model name of the User, defaults to User.
63: * - `scope` Additional conditions to use when looking up and authenticating users,
64: * i.e. `array('User.is_active' => 1).`
65: * - `realm` The realm authentication is for, Defaults to the servername.
66: * - `nonce` A nonce used for authentication. Defaults to `uniqid()`.
67: * - `qop` Defaults to auth, no other values are supported at this time.
68: * - `opaque` A string that must be returned unchanged by clients. 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: 'realm' => '',
80: 'qop' => 'auth',
81: 'nonce' => '',
82: 'opaque' => ''
83: );
84:
85: /**
86: * Constructor, completes configuration for digest authentication.
87: *
88: * @param ComponentCollection $collection The Component collection used on this request.
89: * @param array $settings An array of settings.
90: */
91: public function __construct(ComponentCollection $collection, $settings) {
92: parent::__construct($collection, $settings);
93: if (empty($this->settings['realm'])) {
94: $this->settings['realm'] = env('SERVER_NAME');
95: }
96: if (empty($this->settings['nonce'])) {
97: $this->settings['nonce'] = uniqid('');
98: }
99: if (empty($this->settings['opaque'])) {
100: $this->settings['opaque'] = md5($this->settings['realm']);
101: }
102: }
103:
104: /**
105: * Authenticate a user using Digest HTTP auth. Will use the configured User model and attempt a
106: * login using Digest HTTP auth.
107: *
108: * @param CakeRequest $request The request to authenticate with.
109: * @param CakeResponse $response The response to add headers to.
110: * @return mixed Either false on failure, or an array of user data on success.
111: */
112: public function authenticate(CakeRequest $request, CakeResponse $response) {
113: $user = $this->getUser($request);
114:
115: if (empty($user)) {
116: $response->header($this->loginHeaders());
117: $response->statusCode(401);
118: $response->send();
119: return false;
120: }
121: return $user;
122: }
123:
124: /**
125: * Get a user based on information in the request. Used by cookie-less auth for stateless clients.
126: *
127: * @param CakeRequest $request Request object.
128: * @return mixed Either false or an array of user information
129: */
130: public function getUser($request) {
131: $digest = $this->_getDigest();
132: if (empty($digest)) {
133: return false;
134: }
135: $user = $this->_findUser($digest['username'], null);
136: if (empty($user)) {
137: return false;
138: }
139: $password = $user[$this->settings['fields']['password']];
140: unset($user[$this->settings['fields']['password']]);
141: if ($digest['response'] === $this->generateResponseHash($digest, $password)) {
142: return $user;
143: }
144: return false;
145: }
146:
147: /**
148: * Find a user record using the standard options.
149: *
150: * @param string $username The username/identifier.
151: * @param string $password Unused password, digest doesn't require passwords.
152: * @return Mixed Either false on failure, or an array of user data.
153: */
154: protected function _findUser($username, $password) {
155: $userModel = $this->settings['userModel'];
156: list($plugin, $model) = pluginSplit($userModel);
157: $fields = $this->settings['fields'];
158:
159: $conditions = array(
160: $model . '.' . $fields['username'] => $username,
161: );
162: if (!empty($this->settings['scope'])) {
163: $conditions = array_merge($conditions, $this->settings['scope']);
164: }
165: $result = ClassRegistry::init($userModel)->find('first', array(
166: 'conditions' => $conditions,
167: 'recursive' => 0
168: ));
169: if (empty($result) || empty($result[$model])) {
170: return false;
171: }
172: return $result[$model];
173: }
174:
175: /**
176: * Gets the digest headers from the request/environment.
177: *
178: * @return array Array of digest information.
179: */
180: protected function _getDigest() {
181: $digest = env('PHP_AUTH_DIGEST');
182: if (empty($digest) && function_exists('apache_request_headers')) {
183: $headers = apache_request_headers();
184: if (!empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) == 'Digest ') {
185: $digest = substr($headers['Authorization'], 7);
186: }
187: }
188: if (empty($digest)) {
189: return false;
190: }
191: return $this->parseAuthData($digest);
192: }
193:
194: /**
195: * Parse the digest authentication headers and split them up.
196: *
197: * @param string $digest The raw digest authentication headers.
198: * @return array An array of digest authentication headers
199: */
200: public function parseAuthData($digest) {
201: if (substr($digest, 0, 7) == 'Digest ') {
202: $digest = substr($digest, 7);
203: }
204: $keys = $match = array();
205: $req = array('nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1);
206: preg_match_all('/(\w+)=([\'"]?)([a-zA-Z0-9@=.\/_-]+)\2/', $digest, $match, PREG_SET_ORDER);
207:
208: foreach ($match as $i) {
209: $keys[$i[1]] = $i[3];
210: unset($req[$i[1]]);
211: }
212:
213: if (empty($req)) {
214: return $keys;
215: }
216: return null;
217: }
218:
219: /**
220: * Generate the response hash for a given digest array.
221: *
222: * @param array $digest Digest information containing data from DigestAuthenticate::parseAuthData().
223: * @param string $password The digest hash password generated with DigestAuthenticate::password()
224: * @return string Response hash
225: */
226: public function generateResponseHash($digest, $password) {
227: return md5(
228: $password .
229: ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' .
230: md5(env('REQUEST_METHOD') . ':' . $digest['uri'])
231: );
232: }
233:
234: /**
235: * Creates an auth digest password hash to store
236: *
237: * @param string $username The username to use in the digest hash.
238: * @param string $password The unhashed password to make a digest hash for.
239: * @param string $realm The realm the password is for.
240: * @return string the hashed password that can later be used with Digest authentication.
241: */
242: public static function password($username, $password, $realm) {
243: return md5($username . ':' . $realm . ':' . $password);
244: }
245:
246: /**
247: * Generate the login headers
248: *
249: * @return string Headers for logging in.
250: */
251: public function loginHeaders() {
252: $options = array(
253: 'realm' => $this->settings['realm'],
254: 'qop' => $this->settings['qop'],
255: 'nonce' => $this->settings['nonce'],
256: 'opaque' => $this->settings['opaque']
257: );
258: $opts = array();
259: foreach ($options as $k => $v) {
260: $opts[] = sprintf('%s="%s"', $k, $v);
261: }
262: return 'WWW-Authenticate: Digest ' . implode(',', $opts);
263: }
264: }