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

Skip to content

Commit 7f6f36e

Browse files
[Security] add PasswordUpgraderInterface for opportunistic password migrations
1 parent aa4385d commit 7f6f36e

15 files changed

+226
-17
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/Tests/Security/User/EntityUserProviderTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Bridge\Doctrine\Security\User\EntityUserProvider;
1717
use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper;
1818
use Symfony\Bridge\Doctrine\Tests\Fixtures\User;
19+
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
1920

2021
class EntityUserProviderTest extends TestCase
2122
{
@@ -182,6 +183,23 @@ public function testLoadUserByUserNameShouldDeclineInvalidInterface()
182183
$provider->loadUserByUsername('name');
183184
}
184185

186+
public function testPasswordUpgrades()
187+
{
188+
$user = new User(1, 1, 'user1');
189+
190+
$repository = $this->getMockBuilder(PasswordUpgraderInterface::class)->getMock();
191+
$repository->expects($this->once())
192+
->method('upgradePassword')
193+
->with($user, 'foobar');
194+
195+
$provider = new EntityUserProvider(
196+
$this->getManager($this->getObjectManager($repository)),
197+
'Symfony\Bridge\Doctrine\Tests\Fixtures\User'
198+
);
199+
200+
$provider->upgradePassword($user, 'foobar');
201+
}
202+
185203
private function getManager($em, $name = null)
186204
{
187205
$manager = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry')->getMock();

src/Symfony/Bridge/Doctrine/composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"symfony/property-access": "^3.4|^4.0|^5.0",
3434
"symfony/property-info": "^3.4|^4.0|^5.0",
3535
"symfony/proxy-manager-bridge": "^3.4|^4.0|^5.0",
36-
"symfony/security-core": "^3.4|^4.0|^5.0",
36+
"symfony/security-core": "^4.4|^5.0",
3737
"symfony/expression-language": "^3.4|^4.0|^5.0",
3838
"symfony/validator": "^3.4|^4.0|^5.0",
3939
"symfony/translation": "^3.4|^4.0|^5.0",
@@ -49,7 +49,8 @@
4949
"phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0",
5050
"symfony/dependency-injection": "<3.4",
5151
"symfony/form": "<4.3",
52-
"symfony/messenger": "<4.3"
52+
"symfony/messenger": "<4.3",
53+
"symfony/security-core": "<4.4"
5354
},
5455
"suggest": {
5556
"symfony/form": "",

src/Symfony/Component/Security/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Added method `needsRehash()` to `PasswordEncoderInterface` and `UserPasswordEncoderInterface`
88
* Added `MigratingPasswordEncoder`
9+
* Added and implemented `PasswordUpgraderInterface`
910

1011
4.3.0
1112
-----

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/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider;
1616
use Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder;
1717
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
18+
use Symfony\Component\Security\Core\Tests\Encoder\TestPasswordEncoderInterface;
19+
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
20+
use Symfony\Component\Security\Core\User\User;
21+
use Symfony\Component\Security\Core\User\UserProviderInterface;
1822

1923
class DaoAuthenticationProviderTest extends TestCase
2024
{
@@ -259,6 +263,44 @@ public function testCheckAuthentication()
259263
$method->invoke($provider, $this->getMockBuilder('Symfony\\Component\\Security\\Core\\User\\UserInterface')->getMock(), $token);
260264
}
261265

266+
public function testPasswordUpgrades()
267+
{
268+
$user = new User('user', 'pwd');
269+
270+
$encoder = $this->getMockBuilder(TestPasswordEncoderInterface::class)->getMock();
271+
$encoder->expects($this->once())
272+
->method('isPasswordValid')
273+
->willReturn(true)
274+
;
275+
$encoder->expects($this->once())
276+
->method('encodePassword')
277+
->willReturn('foobar')
278+
;
279+
$encoder->expects($this->once())
280+
->method('needsRehash')
281+
->willReturn(true)
282+
;
283+
284+
$provider = $this->getProvider(null, null, $encoder);
285+
286+
$userProvider = ((array) $provider)[sprintf("\0%s\0userProvider", DaoAuthenticationProvider::class)];
287+
$userProvider->expects($this->once())
288+
->method('upgradePassword')
289+
->with($user, 'foobar')
290+
;
291+
292+
$method = new \ReflectionMethod($provider, 'checkAuthentication');
293+
$method->setAccessible(true);
294+
295+
$token = $this->getSupportedToken();
296+
$token->expects($this->once())
297+
->method('getCredentials')
298+
->willReturn('foo')
299+
;
300+
301+
$method->invoke($provider, $user, $token);
302+
}
303+
262304
protected function getSupportedToken()
263305
{
264306
$mock = $this->getMockBuilder('Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken')->setMethods(['getCredentials', 'getUser', 'getProviderKey'])->disableOriginalConstructor()->getMock();
@@ -273,7 +315,7 @@ protected function getSupportedToken()
273315

274316
protected function getProvider($user = null, $userChecker = null, $passwordEncoder = null)
275317
{
276-
$userProvider = $this->getMockBuilder('Symfony\\Component\\Security\\Core\\User\\UserProviderInterface')->getMock();
318+
$userProvider = $this->getMockBuilder([UserProviderInterface::class, PasswordUpgraderInterface::class])->getMock();
277319
if (null !== $user) {
278320
$userProvider->expects($this->once())
279321
->method('loadUserByUsername')

src/Symfony/Component/Security/Core/Tests/Encoder/MigratingPasswordEncoderTest.php

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Security\Core\Encoder\MigratingPasswordEncoder;
1616
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
17-
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
1817

1918
class MigratingPasswordEncoderTest extends TestCase
2019
{
@@ -66,8 +65,3 @@ public function testFallback()
6665
$this->assertTrue($encoder->isPasswordValid('abc', 'foo', 'salt'));
6766
}
6867
}
69-
70-
interface TestPasswordEncoderInterface extends PasswordEncoderInterface
71-
{
72-
public function needsRehash(string $encoded): bool;
73-
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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\Tests\Encoder;
13+
14+
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
15+
16+
interface TestPasswordEncoderInterface extends PasswordEncoderInterface
17+
{
18+
public function needsRehash(string $encoded): bool;
19+
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
1616
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
1717
use Symfony\Component\Security\Core\User\ChainUserProvider;
18+
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
19+
use Symfony\Component\Security\Core\User\User;
1820

1921
class ChainUserProviderTest extends TestCase
2022
{
@@ -192,6 +194,28 @@ public function testAcceptsTraversable()
192194
$this->assertSame($account, $provider->refreshUser($this->getAccount()));
193195
}
194196

197+
public function testPasswordUpgrades()
198+
{
199+
$user = new User('user', 'pwd');
200+
201+
$provider1 = $this->getMockBuilder(PasswordUpgraderInterface::class)->getMock();
202+
$provider1
203+
->expects($this->once())
204+
->method('upgradePassword')
205+
->willThrowException(new UnsupportedUserException('unsupported'))
206+
;
207+
208+
$provider2 = $this->getMockBuilder(PasswordUpgraderInterface::class)->getMock();
209+
$provider2
210+
->expects($this->once())
211+
->method('upgradePassword')
212+
->with($user, 'foobar')
213+
;
214+
215+
$provider = new ChainUserProvider([$provider1, $provider2]);
216+
$provider->upgradePassword($user, 'foobar');
217+
}
218+
195219
protected function getAccount()
196220
{
197221
return $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock();

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

Lines changed: 17 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,20 @@ 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+
try {
116+
$provider->upgradePassword($user, $newEncodedPassword);
117+
} catch (UnsupportedUserException $e) {
118+
// ignore: password upgrades are opportunistic
119+
}
120+
}
121+
}
122+
}
107123
}

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;
@@ -35,6 +36,7 @@ class LdapUserProvider implements UserProviderInterface
3536
private $defaultSearch;
3637
private $passwordAttribute;
3738
private $extraFields;
39+
private $entry;
3840

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

96+
if (null !== $this->entry) {
97+
// Keep $entry around when called from upgradePassword()
98+
$this->entry = $entry;
99+
}
100+
94101
return $this->loadUser($username, $entry);
95102
}
96103

@@ -114,6 +121,35 @@ public function supportsClass($class)
114121
return 'Symfony\Component\Security\Core\User\User' === $class;
115122
}
116123

124+
/**
125+
* {@inheritdoc}
126+
*/
127+
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
128+
{
129+
if (!$user instanceof User) {
130+
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user)));
131+
}
132+
133+
if (null === $this->passwordAttribute) {
134+
return;
135+
}
136+
137+
try {
138+
// Tell loadUserByUsername() to keep the $entry around
139+
$this->entry = true;
140+
141+
if ($user->isEqualTo($this->loadUserByUsername($user->getUsername())) && \is_object($this->entry)) {
142+
$this->entry->setAttribute($this->passwordAttribute, [$newEncodedPassword]);
143+
$this->ldap->getEntryManager()->update($this->entry);
144+
$user->setPassword($newEncodedPassword);
145+
}
146+
} catch (ExceptionInterface $e) {
147+
// ignore failed password upgrades
148+
} finally {
149+
$this->entry = null;
150+
}
151+
}
152+
117153
/**
118154
* Loads a user from an LDAP entry.
119155
*
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
* Because you don't want your users not being able to log in, this method should be opportunistic:
24+
* it's fine if it does nothing or if it fails without throwing any exception.
25+
*/
26+
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void;
27+
}

0 commit comments

Comments
 (0)