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

Skip to content

Commit 4519f8d

Browse files
[Security] add password rehashing capabilities
1 parent c315767 commit 4519f8d

16 files changed

+241
-10
lines changed

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
* @author Fabien Potencier <[email protected]>
2626
* @author Johannes M. Schmitt <[email protected]>
2727
*/
28-
class EntityUserProvider implements UserProviderInterface
28+
class EntityUserProvider implements UserProviderInterface, PasswordUpgraderInterface
2929
{
3030
private $registry;
3131
private $managerName;
@@ -107,6 +107,22 @@ public function supportsClass($class)
107107
return $class === $this->getClass() || is_subclass_of($class, $this->getClass());
108108
}
109109

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

src/Symfony/Component/Security/CHANGELOG.md

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

4+
4.4.0
5+
-----
6+
7+
* Added `ChainPasswordEncoder`
8+
* Added method `PasswordEncoderInterface::needsRehash()`
9+
* Added and implemented `PasswordUpgraderInterface`
10+
* Deprecated `BCryptPasswordEncoder`, use `NativePasswordEncoder` instead
11+
412
4.3.0
513
-----
614

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: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
* @author Nicolas Grekas <[email protected]>
19+
*/
20+
final class ChainPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface
21+
{
22+
private $bestEncoder;
23+
private $extraEncoders;
24+
25+
public function __construct(PasswordEncoderInterface $bestEncoder, PasswordEncoderInterface ...$extraEncoders)
26+
{
27+
$this->bestEncoder = $bestEncoder;
28+
$this->extraEncoders = $extraEncoders;
29+
}
30+
31+
/**
32+
* {@inheritdoc}
33+
*/
34+
public function encodePassword($raw, $salt)
35+
{
36+
return $this->bestEncoder->encodePassword($raw, $salt);
37+
}
38+
39+
/**
40+
* {@inheritdoc}
41+
*/
42+
public function isPasswordValid($encoded, $raw, $salt)
43+
{
44+
if ($this->bestEncoder->isPasswordValid($encoded, $raw, $salt)) {
45+
return true;
46+
}
47+
48+
foreach ($this->extraEncoders as $encoder) {
49+
if ($encoder->isPasswordValid($encoded, $raw, $salt)) {
50+
return true;
51+
}
52+
}
53+
54+
return false;
55+
}
56+
57+
/**
58+
* {@inheritdoc}
59+
*/
60+
public function needsRehash(string $encoded): bool
61+
{
62+
return $this->bestEncoder->needsRehash($encoded);
63+
}
64+
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,17 @@ 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 ([SodiumPasswordEncoder::isSupported() ? 'sodium' : 'native', 'pbkdf2', $config['hash_algorithm']] as $algo) {
91+
$config['algorithm'] = $algo;
92+
$encoderChain[] = $this->createEncoder($config);
93+
}
94+
95+
return [
96+
'class' => ChainPasswordEncoder::class,
97+
'arguments' => $encoderChain,
98+
];
8999
}
90100

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

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/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/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: 13 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,18 @@ 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+
$user->setPassword($newEncodedPassword);
103+
}
104+
93105
/**
94106
* Returns the user by given username.
95107
*

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Ldap\Entry;
1515
use Symfony\Component\Ldap\Exception\ConnectionException;
16+
use Symfony\Component\Ldap\Exception\ExceptionInterface;
1617
use Symfony\Component\Ldap\LdapInterface;
1718
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
1819
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
@@ -24,7 +25,7 @@
2425
* @author Grégoire Pineau <[email protected]>
2526
* @author Charles Sarrazin <[email protected]>
2627
*/
27-
class LdapUserProvider implements UserProviderInterface
28+
class LdapUserProvider implements UserProviderInterface, PasswordUpgraderInterface
2829
{
2930
private $ldap;
3031
private $baseDn;
@@ -34,6 +35,7 @@ class LdapUserProvider implements UserProviderInterface
3435
private $uidKey;
3536
private $defaultSearch;
3637
private $passwordAttribute;
38+
private $entry;
3739

3840
public function __construct(LdapInterface $ldap, string $baseDn, string $searchDn = null, string $searchPassword = null, array $defaultRoles = [], string $uidKey = null, string $filter = null, string $passwordAttribute = null)
3941
{
@@ -89,6 +91,11 @@ public function loadUserByUsername($username)
8991
} catch (InvalidArgumentException $e) {
9092
}
9193

94+
if (null !== $this->entry) {
95+
// Keep $entry around when called from upgradePassword()
96+
$this->entry = $entry;
97+
}
98+
9299
return $this->loadUser($username, $entry);
93100
}
94101

@@ -112,6 +119,35 @@ public function supportsClass($class)
112119
return 'Symfony\Component\Security\Core\User\User' === $class;
113120
}
114121

122+
/**
123+
* {@inheritdoc}
124+
*/
125+
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
126+
{
127+
if (!$user instanceof User) {
128+
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user)));
129+
}
130+
131+
if (null === $this->passwordAttribute) {
132+
return;
133+
}
134+
135+
try {
136+
// Tell loadUserByUsername() to keep the $entry around
137+
$this->entry = true;
138+
139+
if ($user->isEqualTo($this->loadUserByUsername($user->getUsername())) && \is_object($this->entry)) {
140+
$this->entry->setAttribute($this->passwordAttribute, [$newEncodedPassword]);
141+
$this->ldap->getEntryManager()->update($this->entry);
142+
$user->setPassword($newEncodedPassword);
143+
}
144+
} catch (ExceptionInterface $e) {
145+
// ignore failed password upgrades
146+
} finally {
147+
$this->entry = null;
148+
}
149+
}
150+
115151
/**
116152
* Loads a user from an LDAP entry.
117153
*
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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\User;
13+
14+
/**
15+
* @author Nicolas Grekas <[email protected]>
16+
*/
17+
interface PasswordUpgraderInterface
18+
{
19+
/**
20+
* Upgrades the encoded password of a user, typically for using a better hash algorithm.
21+
*
22+
* This method should persist the new password in the user storage and update the $user object accordingly.
23+
*/
24+
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void;
25+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,9 @@ public function isEqualTo(UserInterface $user)
157157

158158
return true;
159159
}
160+
161+
public function setPassword(string $password)
162+
{
163+
$this->password = $password;
164+
}
160165
}

src/Symfony/Component/Security/Guard/AuthenticatorInterface.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,15 @@ public function getUser($credentials, UserProviderInterface $userProvider);
9494
*
9595
* The *credentials* are the return value from getCredentials()
9696
*
97-
* @param mixed $credentials
98-
* @param UserInterface $user
97+
* @param mixed $credentials
98+
* @param UserInterface $user
99+
* @param PasswordUpgraderInterface $userProvider
99100
*
100101
* @return bool
101102
*
102103
* @throws AuthenticationException
103104
*/
104-
public function checkCredentials($credentials, UserInterface $user);
105+
public function checkCredentials($credentials, UserInterface $user/*, PasswordUpgraderInterface $passwordUpgrader = null*/);
105106

106107
/**
107108
* Create an authenticated token for the given user.

0 commit comments

Comments
 (0)