From d91691573f108422cc2080462af35ebd62dc93fb Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 8 Feb 2015 21:47:36 +0100 Subject: [PATCH] Add custom auth encoder & provider These custom classes allow Wallabag v2 to be compatible with Wallabag v1 salted password --- app/config/security.yml | 62 ++++++------- app/config/services.yml | 3 +- src/Wallabag/CoreBundle/Entity/User.php | 6 +- .../Encoder/WallabagPasswordEncoder.php | 88 ++++++++++++++++++ .../WallabagAuthenticationProvider.php | 89 +++++++++++++++++++ 5 files changed, 215 insertions(+), 33 deletions(-) create mode 100644 src/Wallabag/CoreBundle/Security/Authentication/Encoder/WallabagPasswordEncoder.php create mode 100644 src/Wallabag/CoreBundle/Security/Authentication/Provider/WallabagAuthenticationProvider.php diff --git a/app/config/security.yml b/app/config/security.yml index c1b0fb77..e161c3b5 100644 --- a/app/config/security.yml +++ b/app/config/security.yml @@ -1,6 +1,6 @@ security: encoders: - Wallabag\CoreBundle\Entity\Users: + Wallabag\CoreBundle\Entity\User: algorithm: sha1 encode_as_base64: false iterations: 1 @@ -11,7 +11,7 @@ security: providers: administrators: - entity: { class: WallabagCoreBundle:Users, property: username } + entity: { class: WallabagCoreBundle:User, property: username } # the main part of the security, where you can set up firewalls # for specific sections of your app @@ -23,35 +23,35 @@ security: pattern: ^/login$ anonymous: ~ -# secured_area: -# pattern: ^/ -# anonymous: ~ -# form_login: -# login_path: /login -# -# use_forward: false -# -# check_path: /login_check -# -# post_only: true -# -# always_use_default_target_path: true -# default_target_path: / -# target_path_parameter: redirect_url -# use_referer: true -# -# failure_path: null -# failure_forward: false -# -# username_parameter: _username -# password_parameter: _password -# -# csrf_parameter: _csrf_token -# intention: authenticate -# -# logout: -# path: /logout -# target: / + secured_area: + pattern: ^/ + anonymous: ~ + form_login: + login_path: /login + + use_forward: false + + check_path: /login_check + + post_only: true + + always_use_default_target_path: true + default_target_path: / + target_path_parameter: redirect_url + use_referer: true + + failure_path: null + failure_forward: false + + username_parameter: _username + password_parameter: _password + + csrf_parameter: _csrf_token + intention: authenticate + + logout: + path: /logout + target: / access_control: - { path: ^/api/doc, roles: IS_AUTHENTICATED_ANONYMOUSLY } diff --git a/app/config/services.yml b/app/config/services.yml index 5c76fc59..d4485e42 100644 --- a/app/config/services.yml +++ b/app/config/services.yml @@ -1,7 +1,8 @@ # Learn more about services, parameters and containers at # http://symfony.com/doc/current/book/service_container.html parameters: -# parameter_name: value + security.authentication.provider.dao.class: Wallabag\CoreBundle\Security\Authentication\Provider\WallabagAuthenticationProvider + security.encoder.digest.class: Wallabag\CoreBundle\Security\Authentication\Encoder\WallabagPasswordEncoder services: # service_name: diff --git a/src/Wallabag/CoreBundle/Entity/User.php b/src/Wallabag/CoreBundle/Entity/User.php index cfbd57f8..c83250c3 100644 --- a/src/Wallabag/CoreBundle/Entity/User.php +++ b/src/Wallabag/CoreBundle/Entity/User.php @@ -161,7 +161,11 @@ class User implements AdvancedUserInterface, \Serializable */ public function setPassword($password) { - $this->password = $password; + if (!$password && 0 === strlen($password)) { + return; + } + + $this->password = sha1($password.$this->getUsername().$this->getSalt()); return $this; } diff --git a/src/Wallabag/CoreBundle/Security/Authentication/Encoder/WallabagPasswordEncoder.php b/src/Wallabag/CoreBundle/Security/Authentication/Encoder/WallabagPasswordEncoder.php new file mode 100644 index 00000000..56f1affe --- /dev/null +++ b/src/Wallabag/CoreBundle/Security/Authentication/Encoder/WallabagPasswordEncoder.php @@ -0,0 +1,88 @@ +algorithm = $algorithm; + $this->encodeHashAsBase64 = $encodeHashAsBase64; + $this->iterations = $iterations; + } + + public function setUsername($username) + { + $this->username = $username; + } + + /** + * {@inheritdoc} + */ + public function encodePassword($raw, $salt) + { + if (null === $this->username) { + throw new \LogicException('We can not check the password without a username.'); + } + + if ($this->isPasswordTooLong($raw)) { + throw new BadCredentialsException('Invalid password.'); + } + + if (!in_array($this->algorithm, hash_algos(), true)) { + throw new \LogicException(sprintf('The algorithm "%s" is not supported.', $this->algorithm)); + } + + $salted = $this->mergePasswordAndSalt($raw, $salt); + $digest = hash($this->algorithm, $salted, true); + + // "stretch" hash + for ($i = 1; $i < $this->iterations; $i++) { + $digest = hash($this->algorithm, $digest.$salted, true); + } + + return $this->encodeHashAsBase64 ? base64_encode($digest) : bin2hex($digest); + } + + /** + * {@inheritdoc} + * + * We inject the username inside the salted password + */ + protected function mergePasswordAndSalt($password, $salt) + { + if (empty($salt)) { + return $password; + } + + return $password.$this->username.$salt; + } + + /** + * {@inheritdoc} + */ + public function isPasswordValid($encoded, $raw, $salt) + { + return !$this->isPasswordTooLong($raw) && $this->comparePasswords($encoded, $this->encodePassword($raw, $salt)); + } +} diff --git a/src/Wallabag/CoreBundle/Security/Authentication/Provider/WallabagAuthenticationProvider.php b/src/Wallabag/CoreBundle/Security/Authentication/Provider/WallabagAuthenticationProvider.php new file mode 100644 index 00000000..1c7c5fae --- /dev/null +++ b/src/Wallabag/CoreBundle/Security/Authentication/Provider/WallabagAuthenticationProvider.php @@ -0,0 +1,89 @@ +encoderFactory = $encoderFactory; + $this->userProvider = $userProvider; + } + + /** + * {@inheritdoc} + */ + protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token) + { + $currentUser = $token->getUser(); + if ($currentUser instanceof UserInterface) { + if ($currentUser->getPassword() !== $user->getPassword()) { + throw new BadCredentialsException('The credentials were changed from another session.'); + } + } else { + if ("" === ($presentedPassword = $token->getCredentials())) { + throw new BadCredentialsException('The presented password cannot be empty.'); + } + + // give username, it's used to hash the password + $encoder = $this->encoderFactory->getEncoder($user); + $encoder->setUsername($user->getUsername()); + + if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) { + throw new BadCredentialsException('The presented password is invalid.'); + } + } + } + + /** + * {@inheritdoc} + */ + protected function retrieveUser($username, UsernamePasswordToken $token) + { + $user = $token->getUser(); + if ($user instanceof UserInterface) { + return $user; + } + + try { + $user = $this->userProvider->loadUserByUsername($username); + + if (!$user instanceof UserInterface) { + throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); + } + + return $user; + } catch (UsernameNotFoundException $notFound) { + $notFound->setUsername($username); + throw $notFound; + } catch (\Exception $repositoryProblem) { + $ex = new AuthenticationServiceException($repositoryProblem->getMessage(), 0, $repositoryProblem); + $ex->setToken($token); + throw $ex; + } + } +} -- 2.41.0