1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://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. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 2.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Auth;
16:
17: use Cake\Controller\ComponentRegistry;
18: use Cake\Http\ServerRequest;
19: use Cake\Utility\Security;
20:
21: /**
22: * Digest Authentication adapter for AuthComponent.
23: *
24: * Provides Digest HTTP authentication support for AuthComponent.
25: *
26: * ### Using Digest auth
27: *
28: * Load `AuthComponent` in your controller's `initialize()` and add 'Digest' in 'authenticate' key
29: *
30: * ```
31: * $this->loadComponent('Auth', [
32: * 'authenticate' => ['Digest'],
33: * 'storage' => 'Memory',
34: * 'unauthorizedRedirect' => false,
35: * ]);
36: * ```
37: *
38: * You should set `storage` to `Memory` to prevent CakePHP from sending a
39: * session cookie to the client.
40: *
41: * You should set `unauthorizedRedirect` to `false`. This causes `AuthComponent` to
42: * throw a `ForbiddenException` exception instead of redirecting to another page.
43: *
44: * Since HTTP Digest Authentication is stateless you don't need call `setUser()`
45: * in your controller. The user credentials will be checked on each request. If
46: * valid credentials are not provided, required authentication headers will be sent
47: * by this authentication provider which triggers the login dialog in the browser/client.
48: *
49: * ### Generating passwords compatible with Digest authentication.
50: *
51: * DigestAuthenticate requires a special password hash that conforms to RFC2617.
52: * You can generate this password using `DigestAuthenticate::password()`
53: *
54: * ```
55: * $digestPass = DigestAuthenticate::password($username, $password, env('SERVER_NAME'));
56: * ```
57: *
58: * If you wish to use digest authentication alongside other authentication methods,
59: * it's recommended that you store the digest authentication separately. For
60: * example `User.digest_pass` could be used for a digest password, while
61: * `User.password` would store the password hash for use with other methods like
62: * Basic or Form.
63: *
64: * @see https://book.cakephp.org/3.0/en/controllers/components/authentication.html
65: */
66: class DigestAuthenticate extends BasicAuthenticate
67: {
68:
69: /**
70: * Constructor
71: *
72: * Besides the keys specified in BaseAuthenticate::$_defaultConfig,
73: * DigestAuthenticate uses the following extra keys:
74: *
75: * - `secret` The secret to use for nonce validation. Defaults to Security::getSalt().
76: * - `realm` The realm authentication is for, Defaults to the servername.
77: * - `qop` Defaults to 'auth', no other values are supported at this time.
78: * - `opaque` A string that must be returned unchanged by clients.
79: * Defaults to `md5($config['realm'])`
80: * - `nonceLifetime` The number of seconds that nonces are valid for. Defaults to 300.
81: *
82: * @param \Cake\Controller\ComponentRegistry $registry The Component registry
83: * used on this request.
84: * @param array $config Array of config to use.
85: */
86: public function __construct(ComponentRegistry $registry, array $config = [])
87: {
88: $this->setConfig([
89: 'nonceLifetime' => 300,
90: 'secret' => Security::getSalt(),
91: 'realm' => null,
92: 'qop' => 'auth',
93: 'opaque' => null,
94: ]);
95:
96: parent::__construct($registry, $config);
97: }
98:
99: /**
100: * Get a user based on information in the request. Used by cookie-less auth for stateless clients.
101: *
102: * @param \Cake\Http\ServerRequest $request Request object.
103: * @return mixed Either false or an array of user information
104: */
105: public function getUser(ServerRequest $request)
106: {
107: $digest = $this->_getDigest($request);
108: if (empty($digest)) {
109: return false;
110: }
111:
112: $user = $this->_findUser($digest['username']);
113: if (empty($user)) {
114: return false;
115: }
116:
117: if (!$this->validNonce($digest['nonce'])) {
118: return false;
119: }
120:
121: $field = $this->_config['fields']['password'];
122: $password = $user[$field];
123: unset($user[$field]);
124:
125: $hash = $this->generateResponseHash($digest, $password, $request->getEnv('ORIGINAL_REQUEST_METHOD'));
126: if (hash_equals($hash, $digest['response'])) {
127: return $user;
128: }
129:
130: return false;
131: }
132:
133: /**
134: * Gets the digest headers from the request/environment.
135: *
136: * @param \Cake\Http\ServerRequest $request Request object.
137: * @return array|bool Array of digest information.
138: */
139: protected function _getDigest(ServerRequest $request)
140: {
141: $digest = $request->getEnv('PHP_AUTH_DIGEST');
142: if (empty($digest) && function_exists('apache_request_headers')) {
143: $headers = apache_request_headers();
144: if (!empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) === 'Digest ') {
145: $digest = substr($headers['Authorization'], 7);
146: }
147: }
148: if (empty($digest)) {
149: return false;
150: }
151:
152: return $this->parseAuthData($digest);
153: }
154:
155: /**
156: * Parse the digest authentication headers and split them up.
157: *
158: * @param string $digest The raw digest authentication headers.
159: * @return array|null An array of digest authentication headers
160: */
161: public function parseAuthData($digest)
162: {
163: if (substr($digest, 0, 7) === 'Digest ') {
164: $digest = substr($digest, 7);
165: }
166: $keys = $match = [];
167: $req = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1];
168: preg_match_all('/(\w+)=([\'"]?)([a-zA-Z0-9\:\#\%\?\&@=\.\/_-]+)\2/', $digest, $match, PREG_SET_ORDER);
169:
170: foreach ($match as $i) {
171: $keys[$i[1]] = $i[3];
172: unset($req[$i[1]]);
173: }
174:
175: if (empty($req)) {
176: return $keys;
177: }
178:
179: return null;
180: }
181:
182: /**
183: * Generate the response hash for a given digest array.
184: *
185: * @param array $digest Digest information containing data from DigestAuthenticate::parseAuthData().
186: * @param string $password The digest hash password generated with DigestAuthenticate::password()
187: * @param string $method Request method
188: * @return string Response hash
189: */
190: public function generateResponseHash($digest, $password, $method)
191: {
192: return md5(
193: $password .
194: ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' .
195: md5($method . ':' . $digest['uri'])
196: );
197: }
198:
199: /**
200: * Creates an auth digest password hash to store
201: *
202: * @param string $username The username to use in the digest hash.
203: * @param string $password The unhashed password to make a digest hash for.
204: * @param string $realm The realm the password is for.
205: * @return string the hashed password that can later be used with Digest authentication.
206: */
207: public static function password($username, $password, $realm)
208: {
209: return md5($username . ':' . $realm . ':' . $password);
210: }
211:
212: /**
213: * Generate the login headers
214: *
215: * @param \Cake\Http\ServerRequest $request Request object.
216: * @return array Headers for logging in.
217: */
218: public function loginHeaders(ServerRequest $request)
219: {
220: $realm = $this->_config['realm'] ?: $request->getEnv('SERVER_NAME');
221:
222: $options = [
223: 'realm' => $realm,
224: 'qop' => $this->_config['qop'],
225: 'nonce' => $this->generateNonce(),
226: 'opaque' => $this->_config['opaque'] ?: md5($realm)
227: ];
228:
229: $digest = $this->_getDigest($request);
230: if ($digest && isset($digest['nonce']) && !$this->validNonce($digest['nonce'])) {
231: $options['stale'] = true;
232: }
233:
234: $opts = [];
235: foreach ($options as $k => $v) {
236: if (is_bool($v)) {
237: $v = $v ? 'true' : 'false';
238: $opts[] = sprintf('%s=%s', $k, $v);
239: } else {
240: $opts[] = sprintf('%s="%s"', $k, $v);
241: }
242: }
243:
244: return [
245: 'WWW-Authenticate' => 'Digest ' . implode(',', $opts)
246: ];
247: }
248:
249: /**
250: * Generate a nonce value that is validated in future requests.
251: *
252: * @return string
253: */
254: protected function generateNonce()
255: {
256: $expiryTime = microtime(true) + $this->getConfig('nonceLifetime');
257: $secret = $this->getConfig('secret');
258: $signatureValue = hash_hmac('sha256', $expiryTime . ':' . $secret, $secret);
259: $nonceValue = $expiryTime . ':' . $signatureValue;
260:
261: return base64_encode($nonceValue);
262: }
263:
264: /**
265: * Check the nonce to ensure it is valid and not expired.
266: *
267: * @param string $nonce The nonce value to check.
268: * @return bool
269: */
270: protected function validNonce($nonce)
271: {
272: $value = base64_decode($nonce);
273: if ($value === false) {
274: return false;
275: }
276: $parts = explode(':', $value);
277: if (count($parts) !== 2) {
278: return false;
279: }
280: list($expires, $checksum) = $parts;
281: if ($expires < microtime(true)) {
282: return false;
283: }
284: $secret = $this->getConfig('secret');
285: $check = hash_hmac('sha256', $expires . ':' . $secret, $secret);
286:
287: return hash_equals($check, $checksum);
288: }
289: }
290: