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: * Constructor
70: *
71: * Besides the keys specified in BaseAuthenticate::$_defaultConfig,
72: * DigestAuthenticate uses the following extra keys:
73: *
74: * - `secret` The secret to use for nonce validation. Defaults to Security::getSalt().
75: * - `realm` The realm authentication is for, Defaults to the servername.
76: * - `qop` Defaults to 'auth', no other values are supported at this time.
77: * - `opaque` A string that must be returned unchanged by clients.
78: * Defaults to `md5($config['realm'])`
79: * - `nonceLifetime` The number of seconds that nonces are valid for. Defaults to 300.
80: *
81: * @param \Cake\Controller\ComponentRegistry $registry The Component registry
82: * used on this request.
83: * @param array $config Array of config to use.
84: */
85: public function __construct(ComponentRegistry $registry, array $config = [])
86: {
87: $this->setConfig([
88: 'nonceLifetime' => 300,
89: 'secret' => Security::getSalt(),
90: 'realm' => null,
91: 'qop' => 'auth',
92: 'opaque' => null,
93: ]);
94:
95: parent::__construct($registry, $config);
96: }
97:
98: /**
99: * Get a user based on information in the request. Used by cookie-less auth for stateless clients.
100: *
101: * @param \Cake\Http\ServerRequest $request Request object.
102: * @return array|false Either false or an array of user information
103: */
104: public function getUser(ServerRequest $request)
105: {
106: $digest = $this->_getDigest($request);
107: if (empty($digest)) {
108: return false;
109: }
110:
111: $user = $this->_findUser($digest['username']);
112: if (empty($user)) {
113: return false;
114: }
115:
116: if (!$this->validNonce($digest['nonce'])) {
117: return false;
118: }
119:
120: $field = $this->_config['fields']['password'];
121: $password = $user[$field];
122: unset($user[$field]);
123:
124: $hash = $this->generateResponseHash($digest, $password, $request->getEnv('ORIGINAL_REQUEST_METHOD'));
125: if (hash_equals($hash, $digest['response'])) {
126: return $user;
127: }
128:
129: return false;
130: }
131:
132: /**
133: * Gets the digest headers from the request/environment.
134: *
135: * @param \Cake\Http\ServerRequest $request Request object.
136: * @return array|bool Array of digest information.
137: */
138: protected function _getDigest(ServerRequest $request)
139: {
140: $digest = $request->getEnv('PHP_AUTH_DIGEST');
141: if (empty($digest) && function_exists('apache_request_headers')) {
142: $headers = apache_request_headers();
143: if (!empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) === 'Digest ') {
144: $digest = substr($headers['Authorization'], 7);
145: }
146: }
147: if (empty($digest)) {
148: return false;
149: }
150:
151: return $this->parseAuthData($digest);
152: }
153:
154: /**
155: * Parse the digest authentication headers and split them up.
156: *
157: * @param string $digest The raw digest authentication headers.
158: * @return array|null An array of digest authentication headers
159: */
160: public function parseAuthData($digest)
161: {
162: if (substr($digest, 0, 7) === 'Digest ') {
163: $digest = substr($digest, 7);
164: }
165: $keys = $match = [];
166: $req = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1];
167: preg_match_all('/(\w+)=([\'"]?)([a-zA-Z0-9\:\#\%\?\&@=\.\/_-]+)\2/', $digest, $match, PREG_SET_ORDER);
168:
169: foreach ($match as $i) {
170: $keys[$i[1]] = $i[3];
171: unset($req[$i[1]]);
172: }
173:
174: if (empty($req)) {
175: return $keys;
176: }
177:
178: return null;
179: }
180:
181: /**
182: * Generate the response hash for a given digest array.
183: *
184: * @param array $digest Digest information containing data from DigestAuthenticate::parseAuthData().
185: * @param string $password The digest hash password generated with DigestAuthenticate::password()
186: * @param string $method Request method
187: * @return string Response hash
188: */
189: public function generateResponseHash($digest, $password, $method)
190: {
191: return md5(
192: $password .
193: ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' .
194: md5($method . ':' . $digest['uri'])
195: );
196: }
197:
198: /**
199: * Creates an auth digest password hash to store
200: *
201: * @param string $username The username to use in the digest hash.
202: * @param string $password The unhashed password to make a digest hash for.
203: * @param string $realm The realm the password is for.
204: * @return string the hashed password that can later be used with Digest authentication.
205: */
206: public static function password($username, $password, $realm)
207: {
208: return md5($username . ':' . $realm . ':' . $password);
209: }
210:
211: /**
212: * Generate the login headers
213: *
214: * @param \Cake\Http\ServerRequest $request Request object.
215: * @return string[] Headers for logging in.
216: */
217: public function loginHeaders(ServerRequest $request)
218: {
219: $realm = $this->_config['realm'] ?: $request->getEnv('SERVER_NAME');
220:
221: $options = [
222: 'realm' => $realm,
223: 'qop' => $this->_config['qop'],
224: 'nonce' => $this->generateNonce(),
225: 'opaque' => $this->_config['opaque'] ?: md5($realm)
226: ];
227:
228: $digest = $this->_getDigest($request);
229: if ($digest && isset($digest['nonce']) && !$this->validNonce($digest['nonce'])) {
230: $options['stale'] = true;
231: }
232:
233: $opts = [];
234: foreach ($options as $k => $v) {
235: if (is_bool($v)) {
236: $v = $v ? 'true' : 'false';
237: $opts[] = sprintf('%s=%s', $k, $v);
238: } else {
239: $opts[] = sprintf('%s="%s"', $k, $v);
240: }
241: }
242:
243: return [
244: 'WWW-Authenticate' => 'Digest ' . implode(',', $opts)
245: ];
246: }
247:
248: /**
249: * Generate a nonce value that is validated in future requests.
250: *
251: * @return string
252: */
253: protected function generateNonce()
254: {
255: $expiryTime = microtime(true) + $this->getConfig('nonceLifetime');
256: $secret = $this->getConfig('secret');
257: $signatureValue = hash_hmac('sha256', $expiryTime . ':' . $secret, $secret);
258: $nonceValue = $expiryTime . ':' . $signatureValue;
259:
260: return base64_encode($nonceValue);
261: }
262:
263: /**
264: * Check the nonce to ensure it is valid and not expired.
265: *
266: * @param string $nonce The nonce value to check.
267: * @return bool
268: */
269: protected function validNonce($nonce)
270: {
271: $value = base64_decode($nonce);
272: if ($value === false) {
273: return false;
274: }
275: $parts = explode(':', $value);
276: if (count($parts) !== 2) {
277: return false;
278: }
279: list($expires, $checksum) = $parts;
280: if ($expires < microtime(true)) {
281: return false;
282: }
283: $secret = $this->getConfig('secret');
284: $check = hash_hmac('sha256', $expires . ':' . $secret, $secret);
285:
286: return hash_equals($check, $checksum);
287: }
288: }
289: