Skip to content

Commit

Permalink
Added the net\http\Auth class and refactored net\http\Request and…
Browse files Browse the repository at this point in the history
… `security\auth\adapter\Http` to use it.
  • Loading branch information
gwoo committed Mar 26, 2012
1 parent 358818e commit 693d5c0
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 59 deletions.
103 changes: 103 additions & 0 deletions net/http/Auth.php
@@ -0,0 +1,103 @@
<?php
/**
* Lithium: the most rad php framework
*
* @copyright Copyright 2012, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/

namespace lithium\net\http;

/**
* The `Auth` class handles HTTP Authentication encoding and decode. Typically, this class is not
* used directly, but is a utility of `action\Request` and `security\auth\adapter\Http`
*/
class Auth extends \lithium\core\StaticObject {

/**
* The NC value needed for digest authentication
*
*/
public static $nc = '00000001';

/**
* Returns the proper header string. Accepts the data from the `encode` method.
*
* @param array $data
* @return string
*/
public static function header($data) {
if (empty($data['response'])) {
return null;
}
if (!empty($data['opaque'])) {
$defaults = array(
'realm' => 'app', 'method' => 'GET', 'uri' => '/',
'username' => null, 'qop' => 'auth',
'nonce' => null, 'opaque' => null,
'cnonce' => md5(time()), 'nc' => static::$nc
);
$data += $defaults;
$auth = "username=\"{$data['username']}\", response=\"{$data['response']}\", ";
$auth .= "uri=\"{$data['uri']}\", realm=\"{$data['realm']}\", ";
$auth .= "qop=\"{$data['qop']}\", nc={$data['nc']}, cnonce=\"{$data['cnonce']}\", ";
$auth .= "nonce=\"{$data['nonce']}\", opaque=\"{$data['opaque']}\"";
return "Digest " . $auth;
}
return "Basic " . $data['response'];
}

/**
* Encoded the data with username and password to create the proper response. Returns an array
* containing the username and encoded response.
*
* @param string $username Username to authenticate with
* @param string $password Password to authenticate with
* @param array $data Params needed to hash the response
* @return array
*/
public static function encode($username, $password, $data = array()) {
if (isset($data['nonce'])) {
$defaults = array(
'realm' => 'app', 'method' => 'GET', 'uri' => '/', 'qop' => null,
'cnonce' => md5(time()), 'nc' => static::$nc
);
$data = array_filter($data) + $defaults;
$part1 = md5("{$username}:{$data['realm']}:{$password}");
$part2 = "{$data['nonce']}:{$data['nc']}:{$data['cnonce']}:{$data['qop']}";
$part3 = md5($data['method'] . ':' . $data['uri']);
$response = md5("{$part1}:{$part2}:{$part3}");
return compact('username', 'response') + $data;
}
$response = base64_encode("{$username}:{$password}");
return compact('username', 'response');
}

/**
* Takes the header string and parses out the params needed for a digest authentication.
*
* @param string $header
* @return array
*/
public static function decode($header) {
$data = array(
'realm' => null, 'username' => null, 'uri' => null,
'nonce' => null, 'opaque' => null, 'qop' => null,
'cnonce' => null, 'nc' => null,
'response' => null
);
$keys = implode('|', array_keys($data));
$regex = '@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@';
preg_match_all($regex, $header, $matches, PREG_SET_ORDER);

foreach ($matches as $m) {
if (!isset($m[3]) && !isset($m[4])) {
continue;
}
$data[$m[1]] = $m[3] ? $m[3] : $m[4];
}
return $data;
}
}

?>
5 changes: 4 additions & 1 deletion net/http/Message.php
Expand Up @@ -47,7 +47,10 @@ class Message extends \lithium\net\Message {
*
* @var array
*/
protected $_classes = array('media' => 'lithium\net\http\Media');
protected $_classes = array(
'media' => 'lithium\net\http\Media',
'auth' => 'lithium\net\http\Auth'
);

/**
* Adds config values to the public properties when a new object is created.
Expand Down
25 changes: 8 additions & 17 deletions net/http/Request.php
Expand Up @@ -221,24 +221,15 @@ public function to($format, array $options = array()) {
$options += $defaults;

if (!empty($options['auth'])) {
$auth = null;
if (is_array($options['auth']) && isset($options['auth']['nonce'])) {
$data = $options['auth'];
$nc = '00000001';
$cnonce = md5(time());
$a1 = md5("{$options['username']}:{$data['realm']}:{$options['password']}");
$a2 = md5($options['method'] . ':' . $options['path']);
$nonce = "{$data['nonce']}:{$nc}:{$cnonce}:{$data['qop']}";
$response = md5("{$a1}:{$nonce}:{$a2}");
$auth = "username=\"{$options['username']}\", response=\"{$response}\", ";
$auth .= "uri=\"{$options['path']}\", realm=\"{$data['realm']}\", ";
$auth .= "qop=\"{$data['qop']}\", nc={$nc}, cnonce=\"{$cnonce}\", ";
$auth .= "nonce=\"{$data['nonce']}\", opaque=\"{$data['opaque']}\"";
$this->headers('Authorization', "Digest {$auth}");
} else if (is_string($options['auth']) && $options['auth'] == 'Basic') {
$auth = base64_encode("{$options['username']}:{$options['password']}");
$this->headers('Authorization', "Basic {$auth}");
$data = array();

if (is_array($options['auth']) && !empty($options['auth']['nonce'])) {
$data = array('method' => $options['method'], 'uri' => $options['path']);
$data += $options['auth'];
}
$auth = $this->_classes['auth'];
$data = $auth::encode($options['username'], $options['password'], $data);
$this->headers('Authorization', $auth::header($data));
}
if (in_array($options['method'], array('POST', 'PUT'))) {
$media = $this->_classes['media'];
Expand Down
13 changes: 2 additions & 11 deletions net/http/Response.php
Expand Up @@ -177,17 +177,8 @@ public function digest() {
if (empty($this->headers['WWW-Authenticate'])) {
return array();
}
$header = $this->headers['WWW-Authenticate'];
$params = array('realm' => 1, 'qop' => 1, 'nonce' => 1, 'opaque' => 1);
$keys = implode('|', array_keys($params));
$regex = '@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@';
preg_match_all($regex, $header, $matches, PREG_SET_ORDER);
$result = array();

foreach ($matches as $m) {
$results[$m[1]] = $m[3] ? $m[3] : $m[4];
}
return $results;
$auth = $this->_classes['auth'];
return $auth::decode($this->headers['WWW-Authenticate']);
}

/**
Expand Down
54 changes: 31 additions & 23 deletions security/auth/adapter/Http.php
Expand Up @@ -28,6 +28,15 @@
*/
class Http extends \lithium\core\Object {

/**
* Dynamic class dependencies.
*
* @var array Associative array of class names & their namespaces.
*/
protected $_classes = array(
'auth' => 'lithium\net\http\Auth'
);

/**
* Setup default configuration options.
*
Expand Down Expand Up @@ -91,9 +100,14 @@ public function clear(array $options = array()) {
protected function _basic($request) {
$users = $this->_config['users'];
$username = $request->env('PHP_AUTH_USER');
$password = $request->env('PHP_AUTH_PW');
$auth = $this->_classes['auth'];
$basic = $auth::encode($username, $request->env('PHP_AUTH_PW'));
$encoded = array('response' => null);

if (!isset($users[$username]) || $users[$username] !== $password) {
if (isset($users[$username])) {
$encoded = $auth::encode($username, $users[$username]);
}
if ($basic['response'] !== $encoded['response']) {
$this->_writeHeader("WWW-Authenticate: Basic realm=\"{$this->_config['realm']}\"");
return;
}
Expand All @@ -107,34 +121,28 @@ protected function _basic($request) {
* @return void
*/
protected function _digest($request) {
$realm = $this->_config['realm'];
$data = array(
'username' => null, 'nonce' => null, 'nc' => null,
'cnonce' => null, 'qop' => null, 'uri' => null,
'response' => null
);
$result = array_map(function ($string) use (&$data) {
$parts = explode('=', trim($string), 2) + array('', '');
$data[$parts[0]] = trim($parts[1], '"');
}, explode(',', $request->env('PHP_AUTH_DIGEST')));

$username = $password = null;
$auth = $this->_classes['auth'];
$data = $auth::decode($request->env('PHP_AUTH_DIGEST'));
$data['realm'] = $this->_config['realm'];
$data['method'] = $request->method;
$users = $this->_config['users'];
$password = !empty($users[$data['username']]) ? $users[$data['username']] : null;
$user = md5("{$data['username']}:{$realm}:{$password}");
$nonce = "{$data['nonce']}:{$data['nc']}:{$data['cnonce']}:{$data['qop']}";
$req = md5($request->env('REQUEST_METHOD') . ':' . $data['uri']);
$hash = md5("{$user}:{$nonce}:{$req}");

if (!$data['username'] || $hash !== $data['response']) {
$nonce = uniqid();
$opaque = md5($realm);
if (!empty($data['username']) && !empty($users[$data['username']])) {
$username = $data['username'];
$password = $users[$data['username']];
}
$encoded = $auth::encode($username, $password, $data);

$message = "WWW-Authenticate: Digest realm=\"{$realm}\",qop=\"auth\",";
if ($encoded['response'] !== $data['response']) {
$nonce = uniqid();
$opaque = md5($data['realm']);
$message = "WWW-Authenticate: Digest realm=\"{$data['realm']}\",qop=\"auth\",";
$message .= "nonce=\"{$nonce}\",opaque=\"{$opaque}\"";
$this->_writeHeader($message);
return false;
}
return array('username' => $data['username'], 'password' => $password);
return array('username' => $username, 'password' => $password);
}

/**
Expand Down
106 changes: 106 additions & 0 deletions tests/cases/net/http/AuthTest.php
@@ -0,0 +1,106 @@
<?php
/**
* Lithium: the most rad php framework
*
* @copyright Copyright 2012, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/

namespace lithium\tests\cases\net\http;

use lithium\net\http\Auth;

class AuthTest extends \lithium\test\Unit {

public function testBasicEncode() {
$username = 'gwoo';
$password = 'li3';
$response = base64_encode("{$username}:{$password}");
$expected = compact('username', 'response');
$result = Auth::encode($username, $password);
$this->assertEqual($expected, $result);
}


public function testDigestEncode() {
$username = 'gwoo';
$password = 'li3';
$nc = '00000001';
$cnonce = md5(time());
$user = md5("gwoo:app:li3");
$nonce = "4bca0fbca7bd0:{$nc}:{$cnonce}:auth";
$req = md5("GET:/http_auth");
$response = md5("{$user}:{$nonce}:{$req}");

$data = array(
'realm' => 'app',
'method' => 'GET',
'uri' => '/http_auth',
'qop' => 'auth',
'nonce' => '4bca0fbca7bd0',
'opaque' => 'd3fb67a7aa4d887ec4bf83040a820a46'
);
$expected = $data + compact('username', 'response', 'nc', 'cnonce');
$result = Auth::encode($username, $password, $data);
$this->assertEqual($expected, $result);
}

public function testBasicHeader() {
$username = 'gwoo';
$password = 'li3';
$response = base64_encode("{$username}:{$password}");
$data = Auth::encode($username, $password);
$expected = "Basic " . $response;
$result = Auth::header($data);
$this->assertEqual($expected, $result);
}

public function testDigestHeader() {
$username = 'gwoo';
$password = 'li3';
$nc = '00000001';
$cnonce = md5(time());
$user = md5("gwoo:app:li3");
$nonce = "4bca0fbca7bd0:{$nc}:{$cnonce}:auth";
$req = md5("GET:/http_auth");
$hash = md5("{$user}:{$nonce}:{$req}");

$data = array(
'realm' => 'app',
'method' => 'GET',
'uri' => '/http_auth',
'qop' => 'auth',
'nonce' => '4bca0fbca7bd0',
'opaque' => 'd3fb67a7aa4d887ec4bf83040a820a46'
);
$data = Auth::encode($username, $password, $data);
$header = Auth::header($data);
$this->assertPattern('/Digest/', $header);
preg_match('/response="(.*?)"/', $header, $matches);
list($match, $response) = $matches;

$expected = $hash;
$result = $response;
$this->assertEqual($expected, $result);
}

public function testDecode() {
$header = 'qop="auth",nonce="4bca0fbca7bd0",'
. 'nc="00000001",cnonce="95b2cd1e179bf5414e52ed62811481cf",'
. 'uri="/http_auth",realm="app",'
. 'opaque="d3fb67a7aa4d887ec4bf83040a820a46",username="gwoo",'
. 'response="04d7d878c67f289f37e553d2025e3a52"';

$expected = array(
'qop' => 'auth', 'nonce' => '4bca0fbca7bd0',
'nc' => '00000001', 'cnonce' => '95b2cd1e179bf5414e52ed62811481cf',
'uri' => '/http_auth', 'realm' => 'app',
'opaque' => 'd3fb67a7aa4d887ec4bf83040a820a46', 'username' => 'gwoo',
'response' => '04d7d878c67f289f37e553d2025e3a52'
);
$result = Auth::decode($header);
$this->assertEqual($expected, $result);
}
}

?>
2 changes: 1 addition & 1 deletion tests/cases/net/http/ResponseTest.php
Expand Up @@ -297,7 +297,7 @@ public function testDigestParsing() {
'realm' => 'app', 'qop' => 'auth', 'nonce' => '4ee1617b8756e',
'opaque' => 'dd7bcee161192cb8fba765eb595eba87'
);
$result = $response->digest();
$result = array_filter($response->digest());
$this->assertEqual($expected, $result);
}
}
Expand Down
3 changes: 2 additions & 1 deletion tests/cases/security/auth/adapter/HttpTest.php
Expand Up @@ -59,7 +59,8 @@ public function testCheckDigestIsTrue() {
. 'nc="00000001",cnonce="95b2cd1e179bf5414e52ed62811481cf",'
. 'uri="/http_auth",realm="app",'
. 'opaque="d3fb67a7aa4d887ec4bf83040a820a46",username="gwoo",'
. 'response="04d7d878c67f289f37e553d2025e3a52"')
. 'response="04d7d878c67f289f37e553d2025e3a52"'
)
));
$http = new MockHttp(array('realm' => 'app', 'users' => array('gwoo' => 'li3')));
$result = $http->check($request);
Expand Down
10 changes: 5 additions & 5 deletions tests/mocks/net/http/MockSocket.php
Expand Up @@ -40,14 +40,14 @@ public function read() {
$data['cnonce'] = md5(time());
$username = $this->data->username;
$password = $this->data->password;
$user = md5("{$username}:{$data['realm']}:{$password}");
$nonce = "{$data['nonce']}:{$data['nc']}:{$data['cnonce']}:{$data['qop']}";
$req = md5($this->data->method . ':' . $this->data->path);
$hash = md5("{$user}:{$nonce}:{$req}");
$part1 = md5("{$username}:{$data['realm']}:{$password}");
$part2 = "{$data['nonce']}:{$data['nc']}:{$data['cnonce']}:{$data['qop']}";
$part3 = md5($this->data->method . ':' . $this->data->path);
$hash = md5("{$part1}:{$part2}:{$part3}");
preg_match('/response="(.*?)"/', $this->data->headers('Authorization'), $matches);
list($match, $response) = $matches;

if ($hash == $response) {
if ($hash === $response) {
return 'success';
}
}
Expand Down

0 comments on commit 693d5c0

Please sign in to comment.