diff --git a/cake/libs/controller/components/auth.php b/cake/libs/controller/components/auth.php index 1ab06bd0c..044b6efed 100644 --- a/cake/libs/controller/components/auth.php +++ b/cake/libs/controller/components/auth.php @@ -54,6 +54,13 @@ class AuthComponent extends Object { * @access public */ 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 * '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) * 'object' will validate Controller::action against object::isAuthorized(user, controller, action) * - * @var string + * @var mixed * @access public */ var $authorize = false; @@ -97,13 +104,6 @@ class AuthComponent extends Object { * @access public */ 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 * unspecified, it will be "Auth.{$userModel name}". @@ -262,16 +262,16 @@ class AuthComponent extends Object { return; } - if ($this->allowedActions == array('*') || in_array($controller->action, $this->allowedActions)) { - return false; - } - if (!$this->__setDefaults()) { return false; } $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'])) { $url = ''; } else { @@ -774,6 +774,10 @@ class AuthComponent extends Object { * @return array */ function hashPasswords($data) { + if (is_object($this->authenticate) && method_exists($this->authenticate, 'hashPasswords')) { + return $this->authenticate->hashPasswords($data); + } + if (isset($data[$this->userModel])) { 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']]); @@ -790,7 +794,7 @@ class AuthComponent extends Object { * @return string */ 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. diff --git a/cake/libs/controller/components/security.php b/cake/libs/controller/components/security.php index cb28b6c72..c4eb793a5 100644 --- a/cake/libs/controller/components/security.php +++ b/cake/libs/controller/components/security.php @@ -35,13 +35,6 @@ * @subpackage cake.cake.libs.controller.components */ 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 * @@ -88,7 +81,7 @@ class SecurityComponent extends Object { * @access public * @see SecurityComponent::requireLogin() */ - var $loginOptions = array('type' => ''); + var $loginOptions = array('type' => '', 'prompt' => null); /** * An associative array of usernames/passwords used for HTTP-authenticated logins. * If using digest authentication, passwords should be MD5-hashed. @@ -127,12 +120,6 @@ class SecurityComponent extends Object { * @access public */ var $components = array('RequestHandler', 'Session'); -/** - * Security class initialization - */ - function initialize(&$controller) { - $this->Security =& Security::getInstance(); - } /** * Component startup. All security checking happens here. * @@ -196,6 +183,8 @@ class SecurityComponent extends Object { */ function requireLogin() { $args = func_get_args(); + $base = $this->loginOptions; + foreach ($args as $arg) { if (is_array($arg)) { $this->loginOptions = $arg; @@ -203,6 +192,7 @@ class SecurityComponent extends Object { $this->requireLogin[] = $arg; } } + $this->loginOptions = am($base, $this->loginOptions); if (empty($this->requireLogin)) { $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. * @return mixed If successful, returns an array with login name and password, otherwise null. * @access public */ function loginCredentials($type = null) { - if (empty($type) || low($type) == 'basic') { - $login = array('username' => env('PHP_AUTH_USER'), 'password' => env('PHP_AUTH_PW')); - - if ($login['username'] != null) { - 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); + switch (low($type)) { + case 'basic': + $login = array('username' => env('PHP_AUTH_USER'), 'password' => env('PHP_AUTH_PW')); + if (!empty($login['username'])) { + return $login; } - } else { - // Server doesn't support digest-auth headers - trigger_error(__('SecurityComponent::loginCredentials() - Server does not support digest authentication', true), E_USER_WARNING); - return null; - } + break; + case 'digest': + default: + $digest = null; - if ($digest == null) { - return null; - } - $data = $this->parseDigestAuthData($digest); + 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 { + // 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; } @@ -261,8 +250,16 @@ class SecurityComponent extends Object { function loginRequest($options = array()) { $options = am($this->loginOptions, $options); $this->__setLoginDefaults($options); - $data = 'WWW-Authenticate: ' . ucfirst($options['type']) . ' realm="' . $options['realm'] . '"'; - return $data; + $auth = 'WWW-Authenticate: ' . ucfirst($options['type']); + $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. @@ -291,6 +288,21 @@ class SecurityComponent extends Object { 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 * @@ -398,7 +410,7 @@ class SecurityComponent extends Object { // User hasn't been authenticated yet header($this->loginRequest()); - if (isset($this->loginOptions['prompt'])) { + if (!empty($this->loginOptions['prompt'])) { $this->__callback($controller, $this->loginOptions['prompt']); } else { $this->blackHole($controller, 'login'); @@ -409,6 +421,12 @@ class SecurityComponent extends Object { } else { if (low($this->loginOptions['type']) == 'digest') { // Do digest authentication + if ($login && isset($this->loginUsers[$login['username']])) { + if ($login['response'] == $this->generateDigestResponseHash($login)) { + return true; + } + } + $this->blackHole($controller, 'login'); } else { if (!(in_array($login['username'], array_keys($this->loginUsers)) && $this->loginUsers[$login['username']] == $login['password'])) { $this->blackHole($controller, 'login'); @@ -579,11 +597,12 @@ class SecurityComponent extends Object { * @access private */ function __setLoginDefaults(&$options) { - $options = am(array('type' => 'basic', - 'realm' => env('SERVER_NAME'), - 'qop' => 'auth', - 'nonce' => String::uuid()), - array_filter($options)); + $options = am(array( + 'type' => 'basic', + 'realm' => env('SERVER_NAME'), + 'qop' => 'auth', + 'nonce' => String::uuid() + ), array_filter($options)); $options = am(array('opaque' => md5($options['realm'])), $options); } /** diff --git a/cake/libs/security.php b/cake/libs/security.php index bcb4a859e..450ad318f 100644 --- a/cake/libs/security.php +++ b/cake/libs/security.php @@ -34,7 +34,15 @@ * @package cake * @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. * @@ -106,10 +114,14 @@ class Security extends Object{ * @access public * @static */ - function hash($string, $type = 'sha1') { + function hash($string, $type = null) { $_this =& Security::getInstance(); + if (empty($type)) { + $type = $_this->hashType; + } $type = strtolower($type); - if ($type == 'sha1') { + + if ($type == 'sha1' || $type == null) { if (function_exists('sha1')) { $return = sha1($string); return $return; @@ -132,6 +144,20 @@ class Security extends Object{ 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. *