Refactoring AuthComponent and implementing digest authentication in SecurityComponent

git-svn-id: https://svn.cakephp.org/repo/branches/1.2.x.x@5745 3807eeeb-6ff5-0310-8944-8be069107fe0
This commit is contained in:
nate 2007-10-09 21:00:32 +00:00
parent 736ab28b0b
commit 61c06ae94a
3 changed files with 114 additions and 65 deletions

View file

@ -54,6 +54,13 @@ class AuthComponent extends Object {
* @access public * @access public
*/ */
var $components = array('Session', 'RequestHandler'); var $components = array('Session', 'RequestHandler');
/**
* A reference to the object used for authentication
*
* @var object
* @access public
*/
var $authenticate = null;
/** /**
* The name of the component to use for Authorization or set this to * The name of the component to use for Authorization or set this to
* 'controller' will validate against Controller::isAuthorized() * 'controller' will validate against Controller::isAuthorized()
@ -62,7 +69,7 @@ class AuthComponent extends Object {
* array('model'=> 'name'); will validate mapActions against model $name::isAuthorize(user, controller, mapAction) * array('model'=> 'name'); will validate mapActions against model $name::isAuthorize(user, controller, mapAction)
* 'object' will validate Controller::action against object::isAuthorized(user, controller, action) * 'object' will validate Controller::action against object::isAuthorized(user, controller, action)
* *
* @var string * @var mixed
* @access public * @access public
*/ */
var $authorize = false; var $authorize = false;
@ -97,13 +104,6 @@ class AuthComponent extends Object {
* @access public * @access public
*/ */
var $fields = array('username' => 'username', 'password' => 'password'); var $fields = array('username' => 'username', 'password' => 'password');
/**
* the hash function to use, options: sha1, sha256, md5
*
* @var string
* @access public
*/
var $hash = 'sha1';
/** /**
* The session key name where the record of the current user is stored. If * The session key name where the record of the current user is stored. If
* unspecified, it will be "Auth.{$userModel name}". * unspecified, it will be "Auth.{$userModel name}".
@ -262,16 +262,16 @@ class AuthComponent extends Object {
return; return;
} }
if ($this->allowedActions == array('*') || in_array($controller->action, $this->allowedActions)) {
return false;
}
if (!$this->__setDefaults()) { if (!$this->__setDefaults()) {
return false; return false;
} }
$this->data = $controller->data = $this->hashPasswords($controller->data); $this->data = $controller->data = $this->hashPasswords($controller->data);
if ($this->allowedActions == array('*') || in_array($controller->action, $this->allowedActions)) {
return false;
}
if (!isset($controller->params['url']['url'])) { if (!isset($controller->params['url']['url'])) {
$url = ''; $url = '';
} else { } else {
@ -774,6 +774,10 @@ class AuthComponent extends Object {
* @return array * @return array
*/ */
function hashPasswords($data) { function hashPasswords($data) {
if (is_object($this->authenticate) && method_exists($this->authenticate, 'hashPasswords')) {
return $this->authenticate->hashPasswords($data);
}
if (isset($data[$this->userModel])) { if (isset($data[$this->userModel])) {
if (!empty($data[$this->userModel][$this->fields['username']]) && !empty($data[$this->userModel][$this->fields['password']])) { if (!empty($data[$this->userModel][$this->fields['username']]) && !empty($data[$this->userModel][$this->fields['password']])) {
$data[$this->userModel][$this->fields['password']] = $this->password($data[$this->userModel][$this->fields['password']]); $data[$this->userModel][$this->fields['password']] = $this->password($data[$this->userModel][$this->fields['password']]);
@ -790,7 +794,7 @@ class AuthComponent extends Object {
* @return string * @return string
*/ */
function password($password) { function password($password) {
return Security::hash(CAKE_SESSION_STRING . $password, $this->hash); return Security::hash(CAKE_SESSION_STRING . $password);
} }
/** /**
* Component shutdown. If user is logged in, wipe out redirect. * Component shutdown. If user is logged in, wipe out redirect.

View file

@ -35,13 +35,6 @@
* @subpackage cake.cake.libs.controller.components * @subpackage cake.cake.libs.controller.components
*/ */
class SecurityComponent extends Object { class SecurityComponent extends Object {
/**
* Holds an instance of the core Security object
*
* @var object Security
* @access public
*/
var $Security = null;
/** /**
* The controller method that will be called if this request is black-hole'd * The controller method that will be called if this request is black-hole'd
* *
@ -88,7 +81,7 @@ class SecurityComponent extends Object {
* @access public * @access public
* @see SecurityComponent::requireLogin() * @see SecurityComponent::requireLogin()
*/ */
var $loginOptions = array('type' => ''); var $loginOptions = array('type' => '', 'prompt' => null);
/** /**
* An associative array of usernames/passwords used for HTTP-authenticated logins. * An associative array of usernames/passwords used for HTTP-authenticated logins.
* If using digest authentication, passwords should be MD5-hashed. * If using digest authentication, passwords should be MD5-hashed.
@ -127,12 +120,6 @@ class SecurityComponent extends Object {
* @access public * @access public
*/ */
var $components = array('RequestHandler', 'Session'); var $components = array('RequestHandler', 'Session');
/**
* Security class initialization
*/
function initialize(&$controller) {
$this->Security =& Security::getInstance();
}
/** /**
* Component startup. All security checking happens here. * Component startup. All security checking happens here.
* *
@ -196,6 +183,8 @@ class SecurityComponent extends Object {
*/ */
function requireLogin() { function requireLogin() {
$args = func_get_args(); $args = func_get_args();
$base = $this->loginOptions;
foreach ($args as $arg) { foreach ($args as $arg) {
if (is_array($arg)) { if (is_array($arg)) {
$this->loginOptions = $arg; $this->loginOptions = $arg;
@ -203,6 +192,7 @@ class SecurityComponent extends Object {
$this->requireLogin[] = $arg; $this->requireLogin[] = $arg;
} }
} }
$this->loginOptions = am($base, $this->loginOptions);
if (empty($this->requireLogin)) { if (empty($this->requireLogin)) {
$this->requireLogin = array('*'); $this->requireLogin = array('*');
@ -213,41 +203,40 @@ class SecurityComponent extends Object {
} }
} }
/** /**
* Gets the login credentials for an HTTP-authenticated request * Attempts to validate the login credentials for an HTTP-authenticated request
* *
* @param string $type Either 'basic', 'digest', or null. If null/empty, will try both. * @param string $type Either 'basic', 'digest', or null. If null/empty, will try both.
* @return mixed If successful, returns an array with login name and password, otherwise null. * @return mixed If successful, returns an array with login name and password, otherwise null.
* @access public * @access public
*/ */
function loginCredentials($type = null) { function loginCredentials($type = null) {
if (empty($type) || low($type) == 'basic') { switch (low($type)) {
$login = array('username' => env('PHP_AUTH_USER'), 'password' => env('PHP_AUTH_PW')); case 'basic':
$login = array('username' => env('PHP_AUTH_USER'), 'password' => env('PHP_AUTH_PW'));
if ($login['username'] != null) { if (!empty($login['username'])) {
return $login; return $login;
}
}
if ($type == '' || low($type) == 'digest') {
$digest = null;
if (version_compare(phpversion(), '5.1') != -1) {
$digest = env('PHP_AUTH_DIGEST');
} elseif (function_exists('apache_request_headers')) {
$headers = apache_request_headers();
if (isset($headers['Authorization']) && !empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) == 'Digest ') {
$digest = substr($headers['Authorization'], 7);
} }
} else { break;
// Server doesn't support digest-auth headers case 'digest':
trigger_error(__('SecurityComponent::loginCredentials() - Server does not support digest authentication', true), E_USER_WARNING); default:
return null; $digest = null;
}
if ($digest == null) { if (version_compare(phpversion(), '5.1') != -1) {
return null; $digest = env('PHP_AUTH_DIGEST');
} } elseif (function_exists('apache_request_headers')) {
$data = $this->parseDigestAuthData($digest); $headers = apache_request_headers();
if (isset($headers['Authorization']) && !empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) == 'Digest ') {
$digest = substr($headers['Authorization'], 7);
}
} else {
// Server doesn't support digest-auth headers
trigger_error(__('SecurityComponent::loginCredentials() - Server does not support digest authentication', true), E_USER_WARNING);
}
if (!empty($digest)) {
return $this->parseDigestAuthData($digest);
}
break;
} }
return null; return null;
} }
@ -261,8 +250,16 @@ class SecurityComponent extends Object {
function loginRequest($options = array()) { function loginRequest($options = array()) {
$options = am($this->loginOptions, $options); $options = am($this->loginOptions, $options);
$this->__setLoginDefaults($options); $this->__setLoginDefaults($options);
$data = 'WWW-Authenticate: ' . ucfirst($options['type']) . ' realm="' . $options['realm'] . '"'; $auth = 'WWW-Authenticate: ' . ucfirst($options['type']);
return $data; $out = array('realm="' . $options['realm'] . '"');
if (low($options['type']) == 'digest') {
$out[] = 'qop="auth"';
$out[] = 'nonce="' . uniqid() . '"'; //str_replace('-', '', String::uuid())
$out[] = 'opaque="' . md5($options['realm']).'"';
}
return $auth . ' ' . join(',', $out);
} }
/** /**
* Parses an HTTP digest authentication response, and returns an array of the data, or null on failure. * Parses an HTTP digest authentication response, and returns an array of the data, or null on failure.
@ -291,6 +288,21 @@ class SecurityComponent extends Object {
return null; return null;
} }
} }
/**
* Generates a hash to be compared with an HTTP digest-authenticated response
*
* @param array $data HTTP digest response data, as parsed by SecurityComponent::parseDigestAuthData()
* @return string Digest authentication hash
* @access public
* @see SecurityComponent::parseDigestAuthData()
*/
function generateDigestResponseHash($data) {
return md5(
md5($data['username'] . ':' . $this->loginOptions['realm'] . ':' . $this->loginUsers[$data['username']]) .
':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' .
md5(env('REQUEST_METHOD') . ':' . $data['uri'])
);
}
/** /**
* Black-hole an invalid request with a 404 error or custom callback * Black-hole an invalid request with a 404 error or custom callback
* *
@ -398,7 +410,7 @@ class SecurityComponent extends Object {
// User hasn't been authenticated yet // User hasn't been authenticated yet
header($this->loginRequest()); header($this->loginRequest());
if (isset($this->loginOptions['prompt'])) { if (!empty($this->loginOptions['prompt'])) {
$this->__callback($controller, $this->loginOptions['prompt']); $this->__callback($controller, $this->loginOptions['prompt']);
} else { } else {
$this->blackHole($controller, 'login'); $this->blackHole($controller, 'login');
@ -409,6 +421,12 @@ class SecurityComponent extends Object {
} else { } else {
if (low($this->loginOptions['type']) == 'digest') { if (low($this->loginOptions['type']) == 'digest') {
// Do digest authentication // Do digest authentication
if ($login && isset($this->loginUsers[$login['username']])) {
if ($login['response'] == $this->generateDigestResponseHash($login)) {
return true;
}
}
$this->blackHole($controller, 'login');
} else { } else {
if (!(in_array($login['username'], array_keys($this->loginUsers)) && $this->loginUsers[$login['username']] == $login['password'])) { if (!(in_array($login['username'], array_keys($this->loginUsers)) && $this->loginUsers[$login['username']] == $login['password'])) {
$this->blackHole($controller, 'login'); $this->blackHole($controller, 'login');
@ -579,11 +597,12 @@ class SecurityComponent extends Object {
* @access private * @access private
*/ */
function __setLoginDefaults(&$options) { function __setLoginDefaults(&$options) {
$options = am(array('type' => 'basic', $options = am(array(
'realm' => env('SERVER_NAME'), 'type' => 'basic',
'qop' => 'auth', 'realm' => env('SERVER_NAME'),
'nonce' => String::uuid()), 'qop' => 'auth',
array_filter($options)); 'nonce' => String::uuid()
), array_filter($options));
$options = am(array('opaque' => md5($options['realm'])), $options); $options = am(array('opaque' => md5($options['realm'])), $options);
} }
/** /**

View file

@ -34,7 +34,15 @@
* @package cake * @package cake
* @subpackage cake.cake.libs * @subpackage cake.cake.libs
*/ */
class Security extends Object{ class Security extends Object {
/**
* Default hash method
*
* @var string
* @access public
*/
var $hashType = null;
/** /**
* Singleton implementation to get object instance. * Singleton implementation to get object instance.
* *
@ -106,10 +114,14 @@ class Security extends Object{
* @access public * @access public
* @static * @static
*/ */
function hash($string, $type = 'sha1') { function hash($string, $type = null) {
$_this =& Security::getInstance(); $_this =& Security::getInstance();
if (empty($type)) {
$type = $_this->hashType;
}
$type = strtolower($type); $type = strtolower($type);
if ($type == 'sha1') {
if ($type == 'sha1' || $type == null) {
if (function_exists('sha1')) { if (function_exists('sha1')) {
$return = sha1($string); $return = sha1($string);
return $return; return $return;
@ -132,6 +144,20 @@ class Security extends Object{
return $return; return $return;
} }
} }
/**
* Sets the default hash method for the Security object. This affects all objects using
* Security::hash().
*
* @param string $hash Method to use (sha1/sha256/md5)
* @return void
* @access public
* @static
* @see Security::hash()
*/
function setHash($hash) {
$_this =& Security::getInstance();
$_this->hashType = $hash;
}
/** /**
* Encripts/Decrypts a text using the given key. * Encripts/Decrypts a text using the given key.
* *