diff options
author | Jeremy <jeremy.benoist@gmail.com> | 2015-02-08 21:47:36 +0100 |
---|---|---|
committer | Jeremy <jeremy.benoist@gmail.com> | 2015-02-08 23:13:40 +0100 |
commit | d91691573f108422cc2080462af35ebd62dc93fb (patch) | |
tree | dfef307e3c79e681cbc066c7b18e91f97a294268 | |
parent | 7812f508bcb68d9d0e1868fa568d7a435e7975b7 (diff) | |
download | wallabag-d91691573f108422cc2080462af35ebd62dc93fb.tar.gz wallabag-d91691573f108422cc2080462af35ebd62dc93fb.tar.zst wallabag-d91691573f108422cc2080462af35ebd62dc93fb.zip |
Add custom auth encoder & provider
These custom classes allow Wallabag v2 to be compatible with Wallabag v1 salted password
5 files changed, 215 insertions, 33 deletions
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 @@ | |||
1 | security: | 1 | security: |
2 | encoders: | 2 | encoders: |
3 | Wallabag\CoreBundle\Entity\Users: | 3 | Wallabag\CoreBundle\Entity\User: |
4 | algorithm: sha1 | 4 | algorithm: sha1 |
5 | encode_as_base64: false | 5 | encode_as_base64: false |
6 | iterations: 1 | 6 | iterations: 1 |
@@ -11,7 +11,7 @@ security: | |||
11 | 11 | ||
12 | providers: | 12 | providers: |
13 | administrators: | 13 | administrators: |
14 | entity: { class: WallabagCoreBundle:Users, property: username } | 14 | entity: { class: WallabagCoreBundle:User, property: username } |
15 | 15 | ||
16 | # the main part of the security, where you can set up firewalls | 16 | # the main part of the security, where you can set up firewalls |
17 | # for specific sections of your app | 17 | # for specific sections of your app |
@@ -23,35 +23,35 @@ security: | |||
23 | pattern: ^/login$ | 23 | pattern: ^/login$ |
24 | anonymous: ~ | 24 | anonymous: ~ |
25 | 25 | ||
26 | # secured_area: | 26 | secured_area: |
27 | # pattern: ^/ | 27 | pattern: ^/ |
28 | # anonymous: ~ | 28 | anonymous: ~ |
29 | # form_login: | 29 | form_login: |
30 | # login_path: /login | 30 | login_path: /login |
31 | # | 31 | |
32 | # use_forward: false | 32 | use_forward: false |
33 | # | 33 | |
34 | # check_path: /login_check | 34 | check_path: /login_check |
35 | # | 35 | |
36 | # post_only: true | 36 | post_only: true |
37 | # | 37 | |
38 | # always_use_default_target_path: true | 38 | always_use_default_target_path: true |
39 | # default_target_path: / | 39 | default_target_path: / |
40 | # target_path_parameter: redirect_url | 40 | target_path_parameter: redirect_url |
41 | # use_referer: true | 41 | use_referer: true |
42 | # | 42 | |
43 | # failure_path: null | 43 | failure_path: null |
44 | # failure_forward: false | 44 | failure_forward: false |
45 | # | 45 | |
46 | # username_parameter: _username | 46 | username_parameter: _username |
47 | # password_parameter: _password | 47 | password_parameter: _password |
48 | # | 48 | |
49 | # csrf_parameter: _csrf_token | 49 | csrf_parameter: _csrf_token |
50 | # intention: authenticate | 50 | intention: authenticate |
51 | # | 51 | |
52 | # logout: | 52 | logout: |
53 | # path: /logout | 53 | path: /logout |
54 | # target: / | 54 | target: / |
55 | 55 | ||
56 | access_control: | 56 | access_control: |
57 | - { path: ^/api/doc, roles: IS_AUTHENTICATED_ANONYMOUSLY } | 57 | - { 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 @@ | |||
1 | # Learn more about services, parameters and containers at | 1 | # Learn more about services, parameters and containers at |
2 | # http://symfony.com/doc/current/book/service_container.html | 2 | # http://symfony.com/doc/current/book/service_container.html |
3 | parameters: | 3 | parameters: |
4 | # parameter_name: value | 4 | security.authentication.provider.dao.class: Wallabag\CoreBundle\Security\Authentication\Provider\WallabagAuthenticationProvider |
5 | security.encoder.digest.class: Wallabag\CoreBundle\Security\Authentication\Encoder\WallabagPasswordEncoder | ||
5 | 6 | ||
6 | services: | 7 | services: |
7 | # service_name: | 8 | # 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 | |||
161 | */ | 161 | */ |
162 | public function setPassword($password) | 162 | public function setPassword($password) |
163 | { | 163 | { |
164 | $this->password = $password; | 164 | if (!$password && 0 === strlen($password)) { |
165 | return; | ||
166 | } | ||
167 | |||
168 | $this->password = sha1($password.$this->getUsername().$this->getSalt()); | ||
165 | 169 | ||
166 | return $this; | 170 | return $this; |
167 | } | 171 | } |
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 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Security\Authentication\Encoder; | ||
4 | |||
5 | use Symfony\Component\Security\Core\Encoder\BasePasswordEncoder; | ||
6 | use Symfony\Component\Security\Core\Exception\BadCredentialsException; | ||
7 | |||
8 | /** | ||
9 | * This override just add en extra variable (username) to be able to salt the password | ||
10 | * the way Wallabag v1 does. It will avoid to break compatibility with Wallabag v1 | ||
11 | * | ||
12 | */ | ||
13 | class WallabagPasswordEncoder extends BasePasswordEncoder | ||
14 | { | ||
15 | private $algorithm; | ||
16 | private $encodeHashAsBase64; | ||
17 | private $iterations; | ||
18 | private $username = null; | ||
19 | |||
20 | /** | ||
21 | * Constructor. | ||
22 | * | ||
23 | * @param string $algorithm The digest algorithm to use | ||
24 | * @param bool $encodeHashAsBase64 Whether to base64 encode the password hash | ||
25 | * @param int $iterations The number of iterations to use to stretch the password hash | ||
26 | */ | ||
27 | public function __construct($algorithm = 'sha512', $encodeHashAsBase64 = true, $iterations = 5000) | ||
28 | { | ||
29 | $this->algorithm = $algorithm; | ||
30 | $this->encodeHashAsBase64 = $encodeHashAsBase64; | ||
31 | $this->iterations = $iterations; | ||
32 | } | ||
33 | |||
34 | public function setUsername($username) | ||
35 | { | ||
36 | $this->username = $username; | ||
37 | } | ||
38 | |||
39 | /** | ||
40 | * {@inheritdoc} | ||
41 | */ | ||
42 | public function encodePassword($raw, $salt) | ||
43 | { | ||
44 | if (null === $this->username) { | ||
45 | throw new \LogicException('We can not check the password without a username.'); | ||
46 | } | ||
47 | |||
48 | if ($this->isPasswordTooLong($raw)) { | ||
49 | throw new BadCredentialsException('Invalid password.'); | ||
50 | } | ||
51 | |||
52 | if (!in_array($this->algorithm, hash_algos(), true)) { | ||
53 | throw new \LogicException(sprintf('The algorithm "%s" is not supported.', $this->algorithm)); | ||
54 | } | ||
55 | |||
56 | $salted = $this->mergePasswordAndSalt($raw, $salt); | ||
57 | $digest = hash($this->algorithm, $salted, true); | ||
58 | |||
59 | // "stretch" hash | ||
60 | for ($i = 1; $i < $this->iterations; $i++) { | ||
61 | $digest = hash($this->algorithm, $digest.$salted, true); | ||
62 | } | ||
63 | |||
64 | return $this->encodeHashAsBase64 ? base64_encode($digest) : bin2hex($digest); | ||
65 | } | ||
66 | |||
67 | /** | ||
68 | * {@inheritdoc} | ||
69 | * | ||
70 | * We inject the username inside the salted password | ||
71 | */ | ||
72 | protected function mergePasswordAndSalt($password, $salt) | ||
73 | { | ||
74 | if (empty($salt)) { | ||
75 | return $password; | ||
76 | } | ||
77 | |||
78 | return $password.$this->username.$salt; | ||
79 | } | ||
80 | |||
81 | /** | ||
82 | * {@inheritdoc} | ||
83 | */ | ||
84 | public function isPasswordValid($encoded, $raw, $salt) | ||
85 | { | ||
86 | return !$this->isPasswordTooLong($raw) && $this->comparePasswords($encoded, $this->encodePassword($raw, $salt)); | ||
87 | } | ||
88 | } | ||
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 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Security\Authentication\Provider; | ||
4 | |||
5 | use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; | ||
6 | use Symfony\Component\Security\Core\User\UserProviderInterface; | ||
7 | use Symfony\Component\Security\Core\User\UserCheckerInterface; | ||
8 | use Symfony\Component\Security\Core\User\UserInterface; | ||
9 | use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; | ||
10 | use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; | ||
11 | use Symfony\Component\Security\Core\Exception\BadCredentialsException; | ||
12 | use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; | ||
13 | use Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider; | ||
14 | |||
15 | class WallabagAuthenticationProvider extends UserAuthenticationProvider | ||
16 | { | ||
17 | private $encoderFactory; | ||
18 | private $userProvider; | ||
19 | |||
20 | /** | ||
21 | * Constructor. | ||
22 | * | ||
23 | * @param UserProviderInterface $userProvider An UserProviderInterface instance | ||
24 | * @param UserCheckerInterface $userChecker An UserCheckerInterface instance | ||
25 | * @param string $providerKey The provider key | ||
26 | * @param EncoderFactoryInterface $encoderFactory An EncoderFactoryInterface instance | ||
27 | * @param bool $hideUserNotFoundExceptions Whether to hide user not found exception or not | ||
28 | */ | ||
29 | public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, $providerKey, EncoderFactoryInterface $encoderFactory, $hideUserNotFoundExceptions = true) | ||
30 | { | ||
31 | parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions); | ||
32 | |||
33 | $this->encoderFactory = $encoderFactory; | ||
34 | $this->userProvider = $userProvider; | ||
35 | } | ||
36 | |||
37 | /** | ||
38 | * {@inheritdoc} | ||
39 | */ | ||
40 | protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token) | ||
41 | { | ||
42 | $currentUser = $token->getUser(); | ||
43 | if ($currentUser instanceof UserInterface) { | ||
44 | if ($currentUser->getPassword() !== $user->getPassword()) { | ||
45 | throw new BadCredentialsException('The credentials were changed from another session.'); | ||
46 | } | ||
47 | } else { | ||
48 | if ("" === ($presentedPassword = $token->getCredentials())) { | ||
49 | throw new BadCredentialsException('The presented password cannot be empty.'); | ||
50 | } | ||
51 | |||
52 | // give username, it's used to hash the password | ||
53 | $encoder = $this->encoderFactory->getEncoder($user); | ||
54 | $encoder->setUsername($user->getUsername()); | ||
55 | |||
56 | if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) { | ||
57 | throw new BadCredentialsException('The presented password is invalid.'); | ||
58 | } | ||
59 | } | ||
60 | } | ||
61 | |||
62 | /** | ||
63 | * {@inheritdoc} | ||
64 | */ | ||
65 | protected function retrieveUser($username, UsernamePasswordToken $token) | ||
66 | { | ||
67 | $user = $token->getUser(); | ||
68 | if ($user instanceof UserInterface) { | ||
69 | return $user; | ||
70 | } | ||
71 | |||
72 | try { | ||
73 | $user = $this->userProvider->loadUserByUsername($username); | ||
74 | |||
75 | if (!$user instanceof UserInterface) { | ||
76 | throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); | ||
77 | } | ||
78 | |||
79 | return $user; | ||
80 | } catch (UsernameNotFoundException $notFound) { | ||
81 | $notFound->setUsername($username); | ||
82 | throw $notFound; | ||
83 | } catch (\Exception $repositoryProblem) { | ||
84 | $ex = new AuthenticationServiceException($repositoryProblem->getMessage(), 0, $repositoryProblem); | ||
85 | $ex->setToken($token); | ||
86 | throw $ex; | ||
87 | } | ||
88 | } | ||
89 | } | ||