<?php
namespace Cake\Auth;
use Cake\Controller\ComponentRegistry;
use Cake\Http\ServerRequest;
use Cake\Utility\Security;
class DigestAuthenticate extends BasicAuthenticate
{
public function __construct(ComponentRegistry $registry, array $config = [])
{
$this->setConfig([
'nonceLifetime' => 300,
'secret' => Security::getSalt(),
'realm' => null,
'qop' => 'auth',
'opaque' => null,
]);
parent::__construct($registry, $config);
}
public function getUser(ServerRequest $request)
{
$digest = $this->_getDigest($request);
if (empty($digest)) {
return false;
}
$user = $this->_findUser($digest['username']);
if (empty($user)) {
return false;
}
if (!$this->validNonce($digest['nonce'])) {
return false;
}
$field = $this->_config['fields']['password'];
$password = $user[$field];
unset($user[$field]);
$hash = $this->generateResponseHash($digest, $password, $request->getEnv('ORIGINAL_REQUEST_METHOD'));
if (hash_equals($hash, $digest['response'])) {
return $user;
}
return false;
}
protected function _getDigest(ServerRequest $request)
{
$digest = $request->getEnv('PHP_AUTH_DIGEST');
if (empty($digest) && function_exists('apache_request_headers')) {
$headers = apache_request_headers();
if (!empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) === 'Digest ') {
$digest = substr($headers['Authorization'], 7);
}
}
if (empty($digest)) {
return false;
}
return $this->parseAuthData($digest);
}
public function parseAuthData($digest)
{
if (substr($digest, 0, 7) === 'Digest ') {
$digest = substr($digest, 7);
}
$keys = $match = [];
$req = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1];
preg_match_all('/(\w+)=([\'"]?)([a-zA-Z0-9\:\#\%\?\&@=\.\/_-]+)\2/', $digest, $match, PREG_SET_ORDER);
foreach ($match as $i) {
$keys[$i[1]] = $i[3];
unset($req[$i[1]]);
}
if (empty($req)) {
return $keys;
}
return null;
}
public function generateResponseHash($digest, $password, $method)
{
return md5(
$password .
':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' .
md5($method . ':' . $digest['uri'])
);
}
public static function password($username, $password, $realm)
{
return md5($username . ':' . $realm . ':' . $password);
}
public function loginHeaders(ServerRequest $request)
{
$realm = $this->_config['realm'] ?: $request->getEnv('SERVER_NAME');
$options = [
'realm' => $realm,
'qop' => $this->_config['qop'],
'nonce' => $this->generateNonce(),
'opaque' => $this->_config['opaque'] ?: md5($realm)
];
$digest = $this->_getDigest($request);
if ($digest && isset($digest['nonce']) && !$this->validNonce($digest['nonce'])) {
$options['stale'] = true;
}
$opts = [];
foreach ($options as $k => $v) {
if (is_bool($v)) {
$v = $v ? 'true' : 'false';
$opts[] = sprintf('%s=%s', $k, $v);
} else {
$opts[] = sprintf('%s="%s"', $k, $v);
}
}
return [
'WWW-Authenticate' => 'Digest ' . implode(',', $opts)
];
}
protected function generateNonce()
{
$expiryTime = microtime(true) + $this->getConfig('nonceLifetime');
$secret = $this->getConfig('secret');
$signatureValue = hash_hmac('sha256', $expiryTime . ':' . $secret, $secret);
$nonceValue = $expiryTime . ':' . $signatureValue;
return base64_encode($nonceValue);
}
protected function validNonce($nonce)
{
$value = base64_decode($nonce);
if ($value === false) {
return false;
}
$parts = explode(':', $value);
if (count($parts) !== 2) {
return false;
}
list($expires, $checksum) = $parts;
if ($expires < microtime(true)) {
return false;
}
$secret = $this->getConfig('secret');
$check = hash_hmac('sha256', $expires . ':' . $secret, $secret);
return hash_equals($check, $checksum);
}
}