Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 41bc2c0

Browse files
[Security] add password rehashing capabilities
1 parent c315767 commit 41bc2c0

21 files changed

+268
-14
lines changed

src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Doctrine\Common\Persistence\ManagerRegistry;
1515
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
1616
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
17+
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
1718
use Symfony\Component\Security\Core\User\UserInterface;
1819
use Symfony\Component\Security\Core\User\UserProviderInterface;
1920

@@ -25,7 +26,7 @@
2526
* @author Fabien Potencier <[email protected]>
2627
* @author Johannes M. Schmitt <[email protected]>
2728
*/
28-
class EntityUserProvider implements UserProviderInterface
29+
class EntityUserProvider implements UserProviderInterface, PasswordUpgraderInterface
2930
{
3031
private $registry;
3132
private $managerName;
@@ -107,6 +108,22 @@ public function supportsClass($class)
107108
return $class === $this->getClass() || is_subclass_of($class, $this->getClass());
108109
}
109110

111+
/**
112+
* {@inheritdoc}
113+
*/
114+
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
115+
{
116+
$class = $this->getClass();
117+
if (!$user instanceof $class) {
118+
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user)));
119+
}
120+
121+
$repository = $this->getRepository();
122+
if ($repository instanceof PasswordUpgraderInterface) {
123+
$repository->upgradePassword($user, $newEncodedPassword);
124+
}
125+
}
126+
110127
private function getObjectManager()
111128
{
112129
return $this->registry->getManager($this->managerName);

src/Symfony/Bridge/Doctrine/composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"symfony/property-access": "~3.4|~4.0",
3333
"symfony/property-info": "~3.4|~4.0",
3434
"symfony/proxy-manager-bridge": "~3.4|~4.0",
35-
"symfony/security": "~3.4|~4.0",
35+
"symfony/security": "^4.4",
3636
"symfony/expression-language": "~3.4|~4.0",
3737
"symfony/validator": "~3.4|~4.0",
3838
"symfony/translation": "~3.4|~4.0",
@@ -48,7 +48,8 @@
4848
"phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0",
4949
"symfony/dependency-injection": "<3.4",
5050
"symfony/form": "<4.3",
51-
"symfony/messenger": "<4.2"
51+
"symfony/messenger": "<4.2",
52+
"symfony/security-core": "<4.4"
5253
},
5354
"suggest": {
5455
"symfony/form": "",

src/Symfony/Component/Security/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
CHANGELOG
22
=========
33

4+
4.4.0
5+
-----
6+
7+
* Added `ChainPasswordEncoder`
8+
* Added methods `PasswordEncoderInterface::needsRehash()` and `UserPasswordEncoderInterface::needsRehash()`
9+
* Added and implemented `PasswordUpgraderInterface`
10+
411
4.3.0
512
-----
613

src/Symfony/Component/Security/Core/Authentication/Provider/DaoAuthenticationProvider.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
1717
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
1818
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
19+
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
1920
use Symfony\Component\Security\Core\User\UserCheckerInterface;
2021
use Symfony\Component\Security\Core\User\UserInterface;
2122
use Symfony\Component\Security\Core\User\UserProviderInterface;
@@ -54,9 +55,15 @@ protected function checkAuthentication(UserInterface $user, UsernamePasswordToke
5455
throw new BadCredentialsException('The presented password cannot be empty.');
5556
}
5657

57-
if (!$this->encoderFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) {
58+
$encoder = $this->encoderFactory->getEncoder($user);
59+
60+
if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) {
5861
throw new BadCredentialsException('The presented password is invalid.');
5962
}
63+
64+
if ($this->userProvider instanceof PasswordUpgraderInterface && method_exists($encoder, 'needsRehash') && $encoder->needsRehash($user->getPassword())) {
65+
$this->userProvider->upgradePassword($user, $encoder->encodePassword($presentedPassword, $user->getSalt()));
66+
}
6067
}
6168
}
6269

src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ abstract class BasePasswordEncoder implements PasswordEncoderInterface
2020
{
2121
const MAX_PASSWORD_LENGTH = 4096;
2222

23+
/**
24+
* {@inheritdoc}
25+
*/
26+
public function needsRehash(string $encoded): bool
27+
{
28+
return false;
29+
}
30+
2331
/**
2432
* Demerges a merge password and salt string.
2533
*
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Encoder;
13+
14+
/**
15+
* Hashes passwords using the best available encoder.
16+
* Validates them using a chain of encoders.
17+
*
18+
* /!\ Don't put a PlaintextPasswordEncoder in the list as that'd mean a leaked hash
19+
* could be used to authenticate successfully without knowing the cleartext password.
20+
*
21+
* @author Nicolas Grekas <[email protected]>
22+
*/
23+
final class ChainPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface
24+
{
25+
private $bestEncoder;
26+
private $extraEncoders;
27+
28+
public function __construct(PasswordEncoderInterface $bestEncoder, PasswordEncoderInterface ...$extraEncoders)
29+
{
30+
$this->bestEncoder = $bestEncoder;
31+
$this->extraEncoders = $extraEncoders;
32+
}
33+
34+
/**
35+
* {@inheritdoc}
36+
*/
37+
public function encodePassword($raw, $salt)
38+
{
39+
return $this->bestEncoder->encodePassword($raw, $salt);
40+
}
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
public function isPasswordValid($encoded, $raw, $salt)
46+
{
47+
if ($this->bestEncoder->isPasswordValid($encoded, $raw, $salt)) {
48+
return true;
49+
}
50+
51+
if (!$this->bestEncoder->needsRehash($encoded)) {
52+
return false;
53+
}
54+
55+
foreach ($this->extraEncoders as $encoder) {
56+
if ($encoder->isPasswordValid($encoded, $raw, $salt)) {
57+
return true;
58+
}
59+
}
60+
61+
return false;
62+
}
63+
64+
/**
65+
* {@inheritdoc}
66+
*/
67+
public function needsRehash(string $encoded): bool
68+
{
69+
return $this->bestEncoder->needsRehash($encoded);
70+
}
71+
}

src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,19 @@ private function createEncoder(array $config)
8585
private function getEncoderConfigFromAlgorithm($config)
8686
{
8787
if ('auto' === $config['algorithm']) {
88-
$config['algorithm'] = SodiumPasswordEncoder::isSupported() ? 'sodium' : 'native';
88+
$encoderChain = [];
89+
// "plaintext" is not listed as any leaked hashes could then be used to authenticate directly
90+
foreach (['sodium', 'native', 'pbkdf2', $config['hash_algorithm']] as $algo) {
91+
if ('sodium' !== $algo || SodiumPasswordEncoder::isSupported()) {
92+
$config['algorithm'] = $algo;
93+
$encoderChain[] = $this->createEncoder($config);
94+
}
95+
}
96+
97+
return [
98+
'class' => ChainPasswordEncoder::class,
99+
'arguments' => $encoderChain,
100+
];
89101
}
90102

91103
switch ($config['algorithm']) {

src/Symfony/Component/Security/Core/Encoder/MessageDigestPasswordEncoder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,6 @@ public function encodePassword($raw, $salt)
6565
*/
6666
public function isPasswordValid($encoded, $raw, $salt)
6767
{
68-
return !$this->isPasswordTooLong($raw) && $this->comparePasswords($encoded, $this->encodePassword($raw, $salt));
68+
return '$' !== substr($encoded, 0, 1) && !$this->isPasswordTooLong($raw) && $this->comparePasswords($encoded, $this->encodePassword($raw, $salt));
6969
}
7070
}

src/Symfony/Component/Security/Core/Encoder/NativePasswordEncoder.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,12 @@ public function isPasswordValid($encoded, $raw, $salt)
8787

8888
return \strlen($raw) <= self::MAX_PASSWORD_LENGTH && password_verify($raw, $encoded);
8989
}
90+
91+
/**
92+
* {@inheritdoc}
93+
*/
94+
public function needsRehash(string $encoded): bool
95+
{
96+
return password_needs_rehash($encoded, $this->algo, $this->options);
97+
}
9098
}

src/Symfony/Component/Security/Core/Encoder/PasswordEncoderInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
* PasswordEncoderInterface is the interface for all encoders.
1818
*
1919
* @author Fabien Potencier <[email protected]>
20+
*
21+
* @method bool needsRehash(string $encoded)
2022
*/
2123
interface PasswordEncoderInterface
2224
{

src/Symfony/Component/Security/Core/Encoder/Pbkdf2PasswordEncoder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,6 @@ public function encodePassword($raw, $salt)
7272
*/
7373
public function isPasswordValid($encoded, $raw, $salt)
7474
{
75-
return !$this->isPasswordTooLong($raw) && $this->comparePasswords($encoded, $this->encodePassword($raw, $salt));
75+
return '$' !== substr($encoded, 0, 1) && !$this->isPasswordTooLong($raw) && $this->comparePasswords($encoded, $this->encodePassword($raw, $salt));
7676
}
7777
}

src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,20 @@ public function isPasswordValid($encoded, $raw, $salt)
9494

9595
throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.');
9696
}
97+
98+
/**
99+
* {@inheritdoc}
100+
*/
101+
public function needsRehash(string $encoded): bool
102+
{
103+
if (\function_exists('sodium_crypto_pwhash_str_needs_rehash')) {
104+
return \sodium_crypto_pwhash_str_needs_rehash($encoded, $this->opsLimit, $this->memLimit);
105+
}
106+
107+
if (\extension_loaded('libsodium')) {
108+
return \Sodium\crypto_pwhash_str_needs_rehash($encoded, $this->opsLimit, $this->memLimit);
109+
}
110+
111+
throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.');
112+
}
97113
}

src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoder.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,14 @@ public function isPasswordValid(UserInterface $user, $raw)
4646

4747
return $encoder->isPasswordValid($user->getPassword(), $raw, $user->getSalt());
4848
}
49+
50+
/**
51+
* {@inheritdoc}
52+
*/
53+
public function needsRehash(UserInterface $user, string $encoded): bool
54+
{
55+
$encoder = $this->encoderFactory->getEncoder($user);
56+
57+
return method_exists($encoder, 'needsRehash') && $encoder->needsRehash($encoded);
58+
}
4959
}

src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoderInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
* UserPasswordEncoderInterface is the interface for the password encoder service.
1818
*
1919
* @author Ariel Ferrandini <[email protected]>
20+
*
21+
* @method bool needsRehash(UserInterface $user, string $encoded)
2022
*/
2123
interface UserPasswordEncoderInterface
2224
{

src/Symfony/Component/Security/Core/User/ChainUserProvider.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
*
2323
* @author Johannes M. Schmitt <[email protected]>
2424
*/
25-
class ChainUserProvider implements UserProviderInterface
25+
class ChainUserProvider implements UserProviderInterface, PasswordUpgraderInterface
2626
{
2727
private $providers;
2828

@@ -104,4 +104,16 @@ public function supportsClass($class)
104104

105105
return false;
106106
}
107+
108+
/**
109+
* {@inheritdoc}
110+
*/
111+
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
112+
{
113+
foreach ($this->providers as $provider) {
114+
if ($provider instanceof PasswordUpgraderInterface) {
115+
$provider->upgradePassword($user, $newEncodedPassword);
116+
}
117+
}
118+
}
107119
}

src/Symfony/Component/Security/Core/User/InMemoryUserProvider.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
*
2323
* @author Fabien Potencier <[email protected]>
2424
*/
25-
class InMemoryUserProvider implements UserProviderInterface
25+
class InMemoryUserProvider implements UserProviderInterface, PasswordUpgraderInterface
2626
{
2727
private $users;
2828

@@ -90,6 +90,19 @@ public function supportsClass($class)
9090
return 'Symfony\Component\Security\Core\User\User' === $class;
9191
}
9292

93+
/**
94+
* {@inheritdoc}
95+
*/
96+
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
97+
{
98+
if (!$user instanceof User) {
99+
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user)));
100+
}
101+
102+
$this->getUser($user->getUsername())->setPassword($newEncodedPassword);
103+
$user->setPassword($newEncodedPassword);
104+
}
105+
93106
/**
94107
* Returns the user by given username.
95108
*

0 commit comments

Comments
 (0)