From dd2892ad8d0e3a0b09990b0a9ef26c320f1901fa Mon Sep 17 00:00:00 2001 From: ADmad Date: Mon, 20 May 2013 12:14:35 +0530 Subject: [PATCH] Added password hasher --- .../Component/Auth/AbstractPasswordHasher.php | 73 +++++++++++++++++ .../Component/Auth/BaseAuthenticate.php | 80 +++++++++++++++---- .../Component/Auth/BasicAuthenticate.php | 25 ------ .../Component/Auth/BlowfishAuthenticate.php | 40 ++-------- .../Component/Auth/BlowfishPasswordHasher.php | 47 +++++++++++ .../Component/Auth/SimplePasswordHasher.php | 54 +++++++++++++ .../Component/Auth/BasicAuthenticateTest.php | 33 ++++++++ .../Component/Auth/FormAuthenticateTest.php | 48 +++++++++++ 8 files changed, 328 insertions(+), 72 deletions(-) create mode 100644 lib/Cake/Controller/Component/Auth/AbstractPasswordHasher.php create mode 100644 lib/Cake/Controller/Component/Auth/BlowfishPasswordHasher.php create mode 100644 lib/Cake/Controller/Component/Auth/SimplePasswordHasher.php diff --git a/lib/Cake/Controller/Component/Auth/AbstractPasswordHasher.php b/lib/Cake/Controller/Component/Auth/AbstractPasswordHasher.php new file mode 100644 index 000000000..ed96691a5 --- /dev/null +++ b/lib/Cake/Controller/Component/Auth/AbstractPasswordHasher.php @@ -0,0 +1,73 @@ +config($config); + } + +/** + * Get/Set the config + * + * @param array $config Sets config, if null returns existing config + * @return array Returns configs + */ + public function config($config = null) { + if (is_array($config)) { + $this->_config = array_merge($this->_config, $config); + } + return $this->_config; + } + +/** + * Generates password hash. + * + * @param string|array $password Plain text password to hash or array of data + * required to generate password hash. + * @return string Password hash + */ + abstract public function hash($password); + +/** + * Check hash. Generate hash from user provided password string or data array + * and check against existing hash. + * + * @param string|array $password Plain text password to hash or data array. + * @param string Existing hashed password. + * @return boolean True if hashes match else false. + */ + abstract public function check($password, $hashedPassword); + +} diff --git a/lib/Cake/Controller/Component/Auth/BaseAuthenticate.php b/lib/Cake/Controller/Component/Auth/BaseAuthenticate.php index 152c9b615..a108b90db 100644 --- a/lib/Cake/Controller/Component/Auth/BaseAuthenticate.php +++ b/lib/Cake/Controller/Component/Auth/BaseAuthenticate.php @@ -32,6 +32,9 @@ abstract class BaseAuthenticate { * i.e. `array('User.is_active' => 1).` * - `recursive` The value of the recursive key passed to find(). Defaults to 0. * - `contain` Extra models to contain and store in session. + * - `passwordHasher` Password hasher class. Can be a string specifying class name + * or an array containing `className` key, any other keys will be passed as + * settings to the class. Defaults to 'Simple'. * * @var array */ @@ -44,6 +47,7 @@ abstract class BaseAuthenticate { 'scope' => array(), 'recursive' => 0, 'contain' => null, + 'passwordHasher' => 'Simple' ); /** @@ -53,6 +57,13 @@ abstract class BaseAuthenticate { */ protected $_Collection; +/** + * Password hasher instance. + * + * @var AbstractPasswordHasher + */ + protected $_passwordHasher; + /** * Constructor * @@ -67,56 +78,95 @@ abstract class BaseAuthenticate { /** * Find a user record using the standard options. * - * The $conditions parameter can be a (string)username or an array containing conditions for Model::find('first'). If - * the password field is not included in the conditions the password will be returned. + * The $username parameter can be a (string)username or an array containing + * conditions for Model::find('first'). If the $password param is not provided + * the password field will be present in returned array. * - * @param Mixed $conditions The username/identifier, or an array of find conditions. - * @param Mixed $password The password, only use if passing as $conditions = 'username'. - * @return Mixed Either false on failure, or an array of user data. + * @param string|array $username The username/identifier, or an array of find conditions. + * @param string $password The password, only used if $username param is string. + * @return boolean|array Either false on failure, or an array of user data. */ - protected function _findUser($conditions, $password = null) { + protected function _findUser($username, $password = null) { $userModel = $this->settings['userModel']; list(, $model) = pluginSplit($userModel); $fields = $this->settings['fields']; - if (!is_array($conditions)) { + if (is_array($username)) { + $conditions = $username; + } else { if (!$password) { return false; } - $username = $conditions; $conditions = array( - $model . '.' . $fields['username'] => $username, - $model . '.' . $fields['password'] => $this->_password($password), + $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' => $this->settings['recursive'], 'contain' => $this->settings['contain'], )); - if (empty($result) || empty($result[$model])) { + if (empty($result[$model])) { return false; } + $user = $result[$model]; - if ( - isset($conditions[$model . '.' . $fields['password']]) || - isset($conditions[$fields['password']]) - ) { + if ($password) { + if (!$this->passwordHasher()->check($password, $user[$fields['password']])) { + return false; + } unset($user[$fields['password']]); } + unset($result[$model]); return array_merge($user, $result); } +/** + * Return password hasher object + * + * @return AbstractPasswordHasher Password hasher instance + * @throws CakeException If password hasher class not found or + * it does not extend AbstractPasswordHasher + */ + public function passwordHasher() { + if ($this->_passwordHasher) { + return $this->_passwordHasher; + } + + $config = array(); + if (is_string($this->settings['passwordHasher'])) { + $class = $this->settings['passwordHasher']; + } else { + $class = $this->settings['passwordHasher']['className']; + $config = $this->settings['passwordHasher']; + unset($config['className']); + } + list($plugin, $class) = pluginSplit($class, true); + $className = $class . 'PasswordHasher'; + App::uses($className, $plugin . 'Controller/Component/Auth'); + if (!class_exists($className)) { + throw new CakeException(__d('cake_dev', 'Password hasher class "%s" was not found.', $class)); + } + if (!is_subclass_of($className, 'AbstractPasswordHasher')) { + throw new CakeException(__d('cake_dev', 'Password hasher must extend AbstractPasswordHasher class.')); + } + $this->_passwordHasher = new $className($config); + return $this->_passwordHasher; + } + /** * Hash the plain text password so that it matches the hashed/encrypted password * in the datasource. * * @param string $password The plain text password. * @return string The hashed form of the password. + * @deprecated Since 2.4. Use a PasswordHasher class instead. */ protected function _password($password) { return Security::hash($password, null, true); diff --git a/lib/Cake/Controller/Component/Auth/BasicAuthenticate.php b/lib/Cake/Controller/Component/Auth/BasicAuthenticate.php index 201396edd..49ef6096c 100644 --- a/lib/Cake/Controller/Component/Auth/BasicAuthenticate.php +++ b/lib/Cake/Controller/Component/Auth/BasicAuthenticate.php @@ -43,31 +43,6 @@ App::uses('BaseAuthenticate', 'Controller/Component/Auth'); */ class BasicAuthenticate extends BaseAuthenticate { -/** - * Settings for this object. - * - * - `fields` The fields to use to identify a user by. - * - `userModel` The model name of the User, defaults to User. - * - `scope` Additional conditions to use when looking up and authenticating users, - * i.e. `array('User.is_active' => 1).` - * - `recursive` The value of the recursive key passed to find(). Defaults to 0. - * - `contain` Extra models to contain and store in session. - * - `realm` The realm authentication is for. Defaults the server name. - * - * @var array - */ - public $settings = array( - 'fields' => array( - 'username' => 'username', - 'password' => 'password' - ), - 'userModel' => 'User', - 'scope' => array(), - 'recursive' => 0, - 'contain' => null, - 'realm' => '', - ); - /** * Constructor, completes configuration for basic authentication. * diff --git a/lib/Cake/Controller/Component/Auth/BlowfishAuthenticate.php b/lib/Cake/Controller/Component/Auth/BlowfishAuthenticate.php index 533946f73..bb2dc71d8 100644 --- a/lib/Cake/Controller/Component/Auth/BlowfishAuthenticate.php +++ b/lib/Cake/Controller/Component/Auth/BlowfishAuthenticate.php @@ -37,43 +37,19 @@ App::uses('FormAuthenticate', 'Controller/Component/Auth'); * @package Cake.Controller.Component.Auth * @since CakePHP(tm) v 2.3 * @see AuthComponent::$authenticate + * @deprecated Since 2.4. Just use FormAuthenticate with 'passwordHasher' setting set to 'Blowfish' */ class BlowfishAuthenticate extends FormAuthenticate { /** - * Authenticates the identity contained in a request. Will use the `settings.userModel`, and `settings.fields` - * to find POST data that is used to find a matching record in the`settings.userModel`. Will return false if - * there is no post data, either username or password is missing, or if the scope conditions have not been met. + * Constructor. Sets default passwordHasher to Blowfish * - * @param CakeRequest $request The request that contains login information. - * @param CakeResponse $response Unused response object. - * @return mixed False on login failure. An array of User data on success. + * @param ComponentCollection $collection The Component collection used on this request. + * @param array $settings Array of settings to use. */ - public function authenticate(CakeRequest $request, CakeResponse $response) { - $userModel = $this->settings['userModel']; - list(, $model) = pluginSplit($userModel); - - $fields = $this->settings['fields']; - if (!$this->_checkFields($request, $model, $fields)) { - return false; - } - $user = $this->_findUser( - array( - $model . '.' . $fields['username'] => $request->data[$model][$fields['username']], - ) - ); - if (!$user) { - return false; - } - $password = Security::hash( - $request->data[$model][$fields['password']], - 'blowfish', - $user[$fields['password']] - ); - if ($password === $user[$fields['password']]) { - unset($user[$fields['password']]); - return $user; - } - return false; + public function __construct(ComponentCollection $collection, $settings) { + $this->settings['passwordHasher'] = 'Blowfish'; + parent::__construct($collection, $settings); } + } diff --git a/lib/Cake/Controller/Component/Auth/BlowfishPasswordHasher.php b/lib/Cake/Controller/Component/Auth/BlowfishPasswordHasher.php new file mode 100644 index 000000000..88b655383 --- /dev/null +++ b/lib/Cake/Controller/Component/Auth/BlowfishPasswordHasher.php @@ -0,0 +1,47 @@ + null); + +/** + * Generates password hash. + * + * @param string $password Plain text password to hash. + * @return string Password hash + */ + public function hash($password) { + return Security::hash($password, $this->_config['hashType'], true); + } + +/** + * Check hash. Generate hash for user provided password and check against existing hash. + * + * @param string $password Plain text password to hash. + * @param string Existing hashed password. + * @return boolean True if hashes match else false. + */ + public function check($password, $hashedPassword) { + return $hashedPassword === $this->hash($password); + } + +} diff --git a/lib/Cake/Test/Case/Controller/Component/Auth/BasicAuthenticateTest.php b/lib/Cake/Test/Case/Controller/Component/Auth/BasicAuthenticateTest.php index 837758031..5544803dd 100644 --- a/lib/Cake/Test/Case/Controller/Component/Auth/BasicAuthenticateTest.php +++ b/lib/Cake/Test/Case/Controller/Component/Auth/BasicAuthenticateTest.php @@ -188,4 +188,37 @@ class BasicAuthenticateTest extends CakeTestCase { $this->auth->unauthenticated($request, $this->response); } +/** + * testAuthenticateWithBlowfish + * + * @return void + */ + public function testAuthenticateWithBlowfish() { + $hash = Security::hash('password', 'blowfish'); + $this->skipIf(strpos($hash, '$2a$') === false, 'Skipping blowfish tests as hashing is not working'); + + $request = new CakeRequest('posts/index', false); + $request->addParams(array('pass' => array(), 'named' => array())); + + $_SERVER['PHP_AUTH_USER'] = 'mariano'; + $_SERVER['PHP_AUTH_PW'] = 'password'; + + $User = ClassRegistry::init('User'); + $User->updateAll( + array('password' => $User->getDataSource()->value($hash)), + array('User.user' => 'mariano') + ); + + $this->auth->settings['passwordHasher'] = 'Blowfish'; + + $result = $this->auth->authenticate($request, $this->response); + $expected = array( + 'id' => 1, + 'user' => 'mariano', + 'created' => '2007-03-17 01:16:23', + 'updated' => '2007-03-17 01:18:31' + ); + $this->assertEquals($expected, $result); + } + } diff --git a/lib/Cake/Test/Case/Controller/Component/Auth/FormAuthenticateTest.php b/lib/Cake/Test/Case/Controller/Component/Auth/FormAuthenticateTest.php index c3cc7d6bd..68b045ed1 100644 --- a/lib/Cake/Test/Case/Controller/Component/Auth/FormAuthenticateTest.php +++ b/lib/Cake/Test/Case/Controller/Component/Auth/FormAuthenticateTest.php @@ -228,4 +228,52 @@ class FormAuthenticateTest extends CakeTestCase { CakePlugin::unload(); } +/** + * test password hasher settings + * + * @return void + */ + public function testPasswordHasherSettings() { + $this->auth->settings['passwordHasher'] = array( + 'className' => 'Simple', + 'hashType' => 'md5' + ); + + $passwordHasher = $this->auth->passwordHasher(); + $result = $passwordHasher->config(); + $this->assertEquals('md5', $result['hashType']); + + $hash = Security::hash('mypass', 'md5', true); + $User = ClassRegistry::init('User'); + $User->updateAll( + array('password' => $User->getDataSource()->value($hash)), + array('User.user' => 'mariano') + ); + + $request = new CakeRequest('posts/index', false); + $request->data = array('User' => array( + 'user' => 'mariano', + 'password' => 'mypass' + )); + + $result = $this->auth->authenticate($request, $this->response); + $expected = array( + 'id' => 1, + 'user' => 'mariano', + 'created' => '2007-03-17 01:16:23', + 'updated' => '2007-03-17 01:18:31' + ); + $this->assertEquals($expected, $result); + + $this->auth = new FormAuthenticate($this->Collection, array( + 'fields' => array('username' => 'user', 'password' => 'password'), + 'userModel' => 'User' + )); + $this->auth->settings['passwordHasher'] = array( + 'className' => 'Simple', + 'hashType' => 'sha1' + ); + $this->assertFalse($this->auth->authenticate($request, $this->response)); + } + }