Skip to content

Commit

Permalink
Basics of DigestAuth are working.
Browse files Browse the repository at this point in the history
  • Loading branch information
markstory committed Feb 4, 2011
1 parent 945e49a commit 705b328
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 42 deletions.
108 changes: 103 additions & 5 deletions cake/libs/controller/components/auth/digest_authenticate.php
Expand Up @@ -66,18 +66,19 @@ public function __construct($settings) {
* @return mixed Either false on failure, or an array of user data on success.
*/
public function authenticate(CakeRequest $request, CakeResponse $response) {
$username = env('PHP_AUTH_USER');
$pass = env('PHP_AUTH_PW');
$digest = $this->_getDigest();

if (empty($username) || empty($pass)) {
if (empty($digest)) {
$response->header($this->loginHeaders());
$response->send();
return false;
}

$result = $this->_findUser($username, $pass);
$result = $this->_findUser($digest['username'], null);
$password = $result[$this->settings['fields']['password']];
unset($result[$this->settings['fields']['password']]);

if (empty($result)) {
if (empty($result) || $digest['response'] !== $this->generateResponseHash($digest, $password)) {
$response->header($this->loginHeaders());
$response->header('Location', Router::reverse($request));
$response->statusCode(401);
Expand All @@ -87,6 +88,103 @@ public function authenticate(CakeRequest $request, CakeResponse $response) {
return $result;
}

/**
* Find a user record using the standard options.
*
* @param string $username The username/identifier.
* @param string $password Unused password, digest doesn't require passwords.
* @return Mixed Either false on failure, or an array of user data.
*/
protected function _findUser($username, $password) {
$userModel = $this->settings['userModel'];
list($plugin, $model) = pluginSplit($userModel);
$fields = $this->settings['fields'];

$conditions = array(
$model . '.' . $fields['username'] => $username,
);
if (!empty($this->settings['scope'])) {
$conditions = array_merge($conditions, $this->settings['scope']);
}
$result = ClassRegistry::init($userModel)->find('first', array(
'conditions' => $conditions,
'recursive' => 0
));
if (empty($result) || empty($result[$model])) {
return false;
}
return $result[$model];
}

/**
* Gets the digest headers from the request/environment.
*
* @return array Array of digest information.
*/
protected function _getDigest() {
$digest = env('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);
}

/**
* Parse the digest authentication headers and split them up.
*
* @param string $digest The raw digest authentication headers.
* @return array An array of digest authentication headers
*/
public function parseAuthData($digest) {
if (substr($digest, 0, 7) == 'Digest ') {
$digest = substr($digest, 7);
}
$keys = $match = array();
$req = array('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;
}

/**
* Generate the response hash for a given digest array.
*
* @param array $digest Digest information containing data from DigestAuthenticate::parseAuthData().
* @param string $password The digest hash password generated with DigestAuthenticate::password()
* @return string Response hash
*/
public function generateResponseHash($digest, $password) {
return md5(
$password .
':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' .
md5(env('REQUEST_METHOD') . ':' . $digest['uri'])
);
}

/**
* Creates an auth digest password hash to store
*
* @param string $password The unhashed password to make a digest hash for.
* @return string the hashed password that can later be used with Digest authentication.
*/
public static function password($username, $realm, $password) {
return md5($username . ':' . $realm . ':' . $password);
}

/**
* Generate the login headers
*
Expand Down
Expand Up @@ -46,7 +46,7 @@ function setUp() {
'opaque' => '123abc'
));

$password = Security::hash('password', null, true);
$password = DigestAuthenticate::password('mariano', 'localhost', 'cake');
ClassRegistry::init('User')->updateAll(array('password' => '"' . $password . '"'));
$this->server = $_SERVER;
$this->response = $this->getMock('CakeResponse');
Expand Down Expand Up @@ -99,44 +99,36 @@ function testAuthenticateNoData() {
*
* @return void
*/
function testAuthenticateNoUsername() {
function testAuthenticateWrongUsername() {
$request = new CakeRequest('posts/index', false);
$_SERVER['PHP_AUTH_PW'] = 'foobar';

$this->response->expects($this->once())
->method('header')
->with('WWW-Authenticate: Digest realm="localhost",qop="auth",nonce="123",opaque="123abc"');

$this->assertFalse($this->auth->authenticate($request, $this->response));
}
$request->addParams(array('pass' => array(), 'named' => array()));

/**
* test the authenticate method
*
* @return void
*/
function testAuthenticateNoPassword() {
$request = new CakeRequest('posts/index', false);
$_SERVER['PHP_AUTH_USER'] = 'mariano';
$_SERVER['PHP_AUTH_DIGEST'] = <<<DIGEST
Digest username="incorrect_user",
realm="localhost",
nonce="123456",
uri="/dir/index.html",
qop=auth,
nc=00000001,
cnonce="0a4f113b",
response="6629fae49393a05397450978507c4ef1",
opaque="123abc"
DIGEST;

$this->response->expects($this->once())
$this->response->expects($this->at(0))
->method('header')
->with('WWW-Authenticate: Digest realm="localhost",qop="auth",nonce="123",opaque="123abc"');

$this->assertFalse($this->auth->authenticate($request, $this->response));
}

/**
* test the authenticate method
*
* @return void
*/
function testAuthenticateInjection() {
$request = new CakeRequest('posts/index', false);
$request->addParams(array('pass' => array(), 'named' => array()));

$_SERVER['PHP_AUTH_USER'] = '> 1';
$_SERVER['PHP_AUTH_PW'] = "' OR 1 = 1";
$this->response->expects($this->at(1))
->method('header')
->with('Location', Router::reverse($request));

$this->response->expects($this->at(2))
->method('statusCode')
->with(401);

$this->response->expects($this->at(3))
->method('send');

$this->assertFalse($this->auth->authenticate($request, $this->response));
}
Expand All @@ -160,6 +152,7 @@ function testAuthenticateChallenge() {
$result = $this->auth->authenticate($request, $this->response);
$this->assertFalse($result);
}

/**
* test authenticate sucesss
*
Expand All @@ -169,8 +162,17 @@ function testAuthenticateSuccess() {
$request = new CakeRequest('posts/index', false);
$request->addParams(array('pass' => array(), 'named' => array()));

$_SERVER['PHP_AUTH_USER'] = 'mariano';
$_SERVER['PHP_AUTH_PW'] = 'password';
$_SERVER['PHP_AUTH_DIGEST'] = <<<DIGEST
Digest username="mariano",
realm="localhost",
nonce="123",
uri="/dir/index.html",
qop=auth,
nc=1,
cnonce="123",
response="5e064cc2f3b20894a806b2de3edf9536",
opaque="123abc"
DIGEST;

$result = $this->auth->authenticate($request, $this->response);
$expected = array(
Expand All @@ -192,8 +194,17 @@ function testAuthenticateFailReChallenge() {
$request = new CakeRequest('posts/index', false);
$request->addParams(array('pass' => array(), 'named' => array()));

$_SERVER['PHP_AUTH_USER'] = 'mariano';
$_SERVER['PHP_AUTH_PW'] = 'password';
$_SERVER['PHP_AUTH_DIGEST'] = <<<DIGEST
Digest username="mariano",
realm="localhost",
nonce="123",
uri="/dir/index.html",
qop=auth,
nc=1,
cnonce="123",
response="6629fae49393a05397450978507c4ef1",
opaque="123abc"
DIGEST;

$this->response->expects($this->at(0))
->method('header')
Expand All @@ -213,4 +224,72 @@ function testAuthenticateFailReChallenge() {
$this->assertFalse($this->auth->authenticate($request, $this->response));
}

/**
* testParseDigestAuthData method
*
* @access public
* @return void
*/
function testParseAuthData() {
$digest = <<<DIGEST
Digest username="Mufasa",
realm="testrealm@host.com",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
uri="/dir/index.html",
qop=auth,
nc=00000001,
cnonce="0a4f113b",
response="6629fae49393a05397450978507c4ef1",
opaque="5ccc069c403ebaf9f0171e9517f40e41"
DIGEST;
$expected = array(
'username' => 'Mufasa',
'realm' => 'testrealm@host.com',
'nonce' => 'dcd98b7102dd2f0e8b11d0f600bfb0c093',
'uri' => '/dir/index.html',
'qop' => 'auth',
'nc' => '00000001',
'cnonce' => '0a4f113b',
'response' => '6629fae49393a05397450978507c4ef1',
'opaque' => '5ccc069c403ebaf9f0171e9517f40e41'
);
$result = $this->auth->parseAuthData($digest);
$this->assertSame($expected, $result);

$result = $this->auth->parseAuthData('');
$this->assertNull($result);
}

/**
* test parsing digest information with email addresses
*
* @return void
*/
function testParseAuthEmailAddress() {
$digest = <<<DIGEST
Digest username="mark@example.com",
realm="testrealm@host.com",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
uri="/dir/index.html",
qop=auth,
nc=00000001,
cnonce="0a4f113b",
response="6629fae49393a05397450978507c4ef1",
opaque="5ccc069c403ebaf9f0171e9517f40e41"
DIGEST;
$expected = array(
'username' => 'mark@example.com',
'realm' => 'testrealm@host.com',
'nonce' => 'dcd98b7102dd2f0e8b11d0f600bfb0c093',
'uri' => '/dir/index.html',
'qop' => 'auth',
'nc' => '00000001',
'cnonce' => '0a4f113b',
'response' => '6629fae49393a05397450978507c4ef1',
'opaque' => '5ccc069c403ebaf9f0171e9517f40e41'
);
$result = $this->auth->parseAuthData($digest);
$this->assertIdentical($expected, $result);
}

}

0 comments on commit 705b328

Please sign in to comment.