[Security] add support for opportunistic password migrations
This commit is contained in:
parent
3e523ddd51
commit
2cfc5c7dd6
@ -14,6 +14,7 @@ namespace Symfony\Bridge\Doctrine\Security\User;
|
|||||||
use Doctrine\Common\Persistence\ManagerRegistry;
|
use Doctrine\Common\Persistence\ManagerRegistry;
|
||||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||||
|
|
||||||
@ -25,7 +26,7 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
|
|||||||
* @author Fabien Potencier <fabien@symfony.com>
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||||
*/
|
*/
|
||||||
class EntityUserProvider implements UserProviderInterface
|
class EntityUserProvider implements UserProviderInterface, PasswordUpgraderInterface
|
||||||
{
|
{
|
||||||
private $registry;
|
private $registry;
|
||||||
private $managerName;
|
private $managerName;
|
||||||
@ -107,6 +108,22 @@ class EntityUserProvider implements UserProviderInterface
|
|||||||
return $class === $this->getClass() || is_subclass_of($class, $this->getClass());
|
return $class === $this->getClass() || is_subclass_of($class, $this->getClass());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
|
||||||
|
{
|
||||||
|
$class = $this->getClass();
|
||||||
|
if (!$user instanceof $class) {
|
||||||
|
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$repository = $this->getRepository();
|
||||||
|
if ($repository instanceof PasswordUpgraderInterface) {
|
||||||
|
$repository->upgradePassword($user, $newEncodedPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function getObjectManager()
|
private function getObjectManager()
|
||||||
{
|
{
|
||||||
return $this->registry->getManager($this->managerName);
|
return $this->registry->getManager($this->managerName);
|
||||||
|
@ -16,6 +16,7 @@ use PHPUnit\Framework\TestCase;
|
|||||||
use Symfony\Bridge\Doctrine\Security\User\EntityUserProvider;
|
use Symfony\Bridge\Doctrine\Security\User\EntityUserProvider;
|
||||||
use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper;
|
use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper;
|
||||||
use Symfony\Bridge\Doctrine\Tests\Fixtures\User;
|
use Symfony\Bridge\Doctrine\Tests\Fixtures\User;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||||
|
|
||||||
class EntityUserProviderTest extends TestCase
|
class EntityUserProviderTest extends TestCase
|
||||||
{
|
{
|
||||||
@ -175,6 +176,23 @@ class EntityUserProviderTest extends TestCase
|
|||||||
$provider->loadUserByUsername('name');
|
$provider->loadUserByUsername('name');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testPasswordUpgrades()
|
||||||
|
{
|
||||||
|
$user = new User(1, 1, 'user1');
|
||||||
|
|
||||||
|
$repository = $this->getMockBuilder(PasswordUpgraderInterface::class)->getMock();
|
||||||
|
$repository->expects($this->once())
|
||||||
|
->method('upgradePassword')
|
||||||
|
->with($user, 'foobar');
|
||||||
|
|
||||||
|
$provider = new EntityUserProvider(
|
||||||
|
$this->getManager($this->getObjectManager($repository)),
|
||||||
|
'Symfony\Bridge\Doctrine\Tests\Fixtures\User'
|
||||||
|
);
|
||||||
|
|
||||||
|
$provider->upgradePassword($user, 'foobar');
|
||||||
|
}
|
||||||
|
|
||||||
private function getManager($em, $name = null)
|
private function getManager($em, $name = null)
|
||||||
{
|
{
|
||||||
$manager = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry')->getMock();
|
$manager = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry')->getMock();
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
"symfony/property-access": "^3.4|^4.0|^5.0",
|
"symfony/property-access": "^3.4|^4.0|^5.0",
|
||||||
"symfony/property-info": "^3.4|^4.0|^5.0",
|
"symfony/property-info": "^3.4|^4.0|^5.0",
|
||||||
"symfony/proxy-manager-bridge": "^3.4|^4.0|^5.0",
|
"symfony/proxy-manager-bridge": "^3.4|^4.0|^5.0",
|
||||||
"symfony/security-core": "^3.4|^4.0|^5.0",
|
"symfony/security-core": "^4.4|^5.0",
|
||||||
"symfony/expression-language": "^3.4|^4.0|^5.0",
|
"symfony/expression-language": "^3.4|^4.0|^5.0",
|
||||||
"symfony/validator": "^3.4|^4.0|^5.0",
|
"symfony/validator": "^3.4|^4.0|^5.0",
|
||||||
"symfony/translation": "^3.4|^4.0|^5.0",
|
"symfony/translation": "^3.4|^4.0|^5.0",
|
||||||
@ -49,7 +49,8 @@
|
|||||||
"phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0",
|
"phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0",
|
||||||
"symfony/dependency-injection": "<3.4",
|
"symfony/dependency-injection": "<3.4",
|
||||||
"symfony/form": "<4.4",
|
"symfony/form": "<4.4",
|
||||||
"symfony/messenger": "<4.3"
|
"symfony/messenger": "<4.3",
|
||||||
|
"symfony/security-core": "<4.4"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"symfony/form": "",
|
"symfony/form": "",
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
<argument /> <!-- User Provider -->
|
<argument /> <!-- User Provider -->
|
||||||
<argument /> <!-- Provider-shared Key -->
|
<argument /> <!-- Provider-shared Key -->
|
||||||
<argument /> <!-- User Checker -->
|
<argument /> <!-- User Checker -->
|
||||||
|
<argument type="service" id="security.password_encoder" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service id="security.authentication.listener.guard"
|
<service id="security.authentication.listener.guard"
|
||||||
|
@ -13,10 +13,12 @@ namespace Symfony\Component\Ldap\Security;
|
|||||||
|
|
||||||
use Symfony\Component\Ldap\Entry;
|
use Symfony\Component\Ldap\Entry;
|
||||||
use Symfony\Component\Ldap\Exception\ConnectionException;
|
use Symfony\Component\Ldap\Exception\ConnectionException;
|
||||||
|
use Symfony\Component\Ldap\Exception\ExceptionInterface;
|
||||||
use Symfony\Component\Ldap\LdapInterface;
|
use Symfony\Component\Ldap\LdapInterface;
|
||||||
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
|
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
|
||||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||||
|
|
||||||
@ -27,7 +29,7 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
|
|||||||
* @author Charles Sarrazin <charles@sarraz.in>
|
* @author Charles Sarrazin <charles@sarraz.in>
|
||||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||||
*/
|
*/
|
||||||
class LdapUserProvider implements UserProviderInterface
|
class LdapUserProvider implements UserProviderInterface, PasswordUpgraderInterface
|
||||||
{
|
{
|
||||||
private $ldap;
|
private $ldap;
|
||||||
private $baseDn;
|
private $baseDn;
|
||||||
@ -109,6 +111,30 @@ class LdapUserProvider implements UserProviderInterface
|
|||||||
return new LdapUser($user->getEntry(), $user->getUsername(), $user->getPassword(), $user->getRoles());
|
return new LdapUser($user->getEntry(), $user->getUsername(), $user->getPassword(), $user->getRoles());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
|
||||||
|
{
|
||||||
|
if (!$user instanceof LdapUser) {
|
||||||
|
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $this->passwordAttribute) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($user->isEqualTo($this->loadUserByUsername($user->getUsername()))) {
|
||||||
|
$user->getEntry()->setAttribute($this->passwordAttribute, [$newEncodedPassword]);
|
||||||
|
$this->ldap->getEntryManager()->update($user->getEntry());
|
||||||
|
$user->setPassword($newEncodedPassword);
|
||||||
|
}
|
||||||
|
} catch (ExceptionInterface $e) {
|
||||||
|
// ignore failed password upgrades
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
@ -7,6 +7,9 @@ CHANGELOG
|
|||||||
* Deprecated class `LdapUserProvider`, use `Symfony\Component\Ldap\Security\LdapUserProvider` instead
|
* Deprecated class `LdapUserProvider`, use `Symfony\Component\Ldap\Security\LdapUserProvider` instead
|
||||||
* Added method `needsRehash()` to `PasswordEncoderInterface` and `UserPasswordEncoderInterface`
|
* Added method `needsRehash()` to `PasswordEncoderInterface` and `UserPasswordEncoderInterface`
|
||||||
* Added `MigratingPasswordEncoder`
|
* Added `MigratingPasswordEncoder`
|
||||||
|
* Added and implemented `PasswordUpgraderInterface`, for opportunistic password migrations
|
||||||
|
* Added `Guard\PasswordAuthenticatedInterface`, an optional interface
|
||||||
|
for "guard" authenticators that deal with user passwords
|
||||||
|
|
||||||
4.3.0
|
4.3.0
|
||||||
-----
|
-----
|
||||||
|
@ -16,6 +16,7 @@ use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
|||||||
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
|
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
|
||||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||||
@ -54,9 +55,15 @@ class DaoAuthenticationProvider extends UserAuthenticationProvider
|
|||||||
throw new BadCredentialsException('The presented password cannot be empty.');
|
throw new BadCredentialsException('The presented password cannot be empty.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->encoderFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) {
|
$encoder = $this->encoderFactory->getEncoder($user);
|
||||||
|
|
||||||
|
if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) {
|
||||||
throw new BadCredentialsException('The presented password is invalid.');
|
throw new BadCredentialsException('The presented password is invalid.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->userProvider instanceof PasswordUpgraderInterface && method_exists($encoder, 'needsRehash') && $encoder->needsRehash($user->getPassword())) {
|
||||||
|
$this->userProvider->upgradePassword($user, $encoder->encodePassword($presentedPassword, $user->getSalt()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,10 @@ use PHPUnit\Framework\TestCase;
|
|||||||
use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider;
|
use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider;
|
||||||
use Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder;
|
use Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder;
|
||||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||||
|
use Symfony\Component\Security\Core\Tests\Encoder\TestPasswordEncoderInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\User;
|
||||||
|
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||||
|
|
||||||
class DaoAuthenticationProviderTest extends TestCase
|
class DaoAuthenticationProviderTest extends TestCase
|
||||||
{
|
{
|
||||||
@ -247,6 +251,44 @@ class DaoAuthenticationProviderTest extends TestCase
|
|||||||
$method->invoke($provider, $this->getMockBuilder('Symfony\\Component\\Security\\Core\\User\\UserInterface')->getMock(), $token);
|
$method->invoke($provider, $this->getMockBuilder('Symfony\\Component\\Security\\Core\\User\\UserInterface')->getMock(), $token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testPasswordUpgrades()
|
||||||
|
{
|
||||||
|
$user = new User('user', 'pwd');
|
||||||
|
|
||||||
|
$encoder = $this->getMockBuilder(TestPasswordEncoderInterface::class)->getMock();
|
||||||
|
$encoder->expects($this->once())
|
||||||
|
->method('isPasswordValid')
|
||||||
|
->willReturn(true)
|
||||||
|
;
|
||||||
|
$encoder->expects($this->once())
|
||||||
|
->method('encodePassword')
|
||||||
|
->willReturn('foobar')
|
||||||
|
;
|
||||||
|
$encoder->expects($this->once())
|
||||||
|
->method('needsRehash')
|
||||||
|
->willReturn(true)
|
||||||
|
;
|
||||||
|
|
||||||
|
$provider = $this->getProvider(null, null, $encoder);
|
||||||
|
|
||||||
|
$userProvider = ((array) $provider)[sprintf("\0%s\0userProvider", DaoAuthenticationProvider::class)];
|
||||||
|
$userProvider->expects($this->once())
|
||||||
|
->method('upgradePassword')
|
||||||
|
->with($user, 'foobar')
|
||||||
|
;
|
||||||
|
|
||||||
|
$method = new \ReflectionMethod($provider, 'checkAuthentication');
|
||||||
|
$method->setAccessible(true);
|
||||||
|
|
||||||
|
$token = $this->getSupportedToken();
|
||||||
|
$token->expects($this->once())
|
||||||
|
->method('getCredentials')
|
||||||
|
->willReturn('foo')
|
||||||
|
;
|
||||||
|
|
||||||
|
$method->invoke($provider, $user, $token);
|
||||||
|
}
|
||||||
|
|
||||||
protected function getSupportedToken()
|
protected function getSupportedToken()
|
||||||
{
|
{
|
||||||
$mock = $this->getMockBuilder('Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken')->setMethods(['getCredentials', 'getUser', 'getProviderKey'])->disableOriginalConstructor()->getMock();
|
$mock = $this->getMockBuilder('Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken')->setMethods(['getCredentials', 'getUser', 'getProviderKey'])->disableOriginalConstructor()->getMock();
|
||||||
@ -261,7 +303,7 @@ class DaoAuthenticationProviderTest extends TestCase
|
|||||||
|
|
||||||
protected function getProvider($user = null, $userChecker = null, $passwordEncoder = null)
|
protected function getProvider($user = null, $userChecker = null, $passwordEncoder = null)
|
||||||
{
|
{
|
||||||
$userProvider = $this->getMockBuilder('Symfony\\Component\\Security\\Core\\User\\UserProviderInterface')->getMock();
|
$userProvider = $this->getMockBuilder([UserProviderInterface::class, PasswordUpgraderInterface::class])->getMock();
|
||||||
if (null !== $user) {
|
if (null !== $user) {
|
||||||
$userProvider->expects($this->once())
|
$userProvider->expects($this->once())
|
||||||
->method('loadUserByUsername')
|
->method('loadUserByUsername')
|
||||||
|
@ -14,7 +14,6 @@ namespace Symfony\Component\Security\Core\Tests\Encoder;
|
|||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\Security\Core\Encoder\MigratingPasswordEncoder;
|
use Symfony\Component\Security\Core\Encoder\MigratingPasswordEncoder;
|
||||||
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
|
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
|
||||||
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
|
|
||||||
|
|
||||||
class MigratingPasswordEncoderTest extends TestCase
|
class MigratingPasswordEncoderTest extends TestCase
|
||||||
{
|
{
|
||||||
@ -66,8 +65,3 @@ class MigratingPasswordEncoderTest extends TestCase
|
|||||||
$this->assertTrue($encoder->isPasswordValid('abc', 'foo', 'salt'));
|
$this->assertTrue($encoder->isPasswordValid('abc', 'foo', 'salt'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TestPasswordEncoderInterface extends PasswordEncoderInterface
|
|
||||||
{
|
|
||||||
public function needsRehash(string $encoded): bool;
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Security\Core\Tests\Encoder;
|
||||||
|
|
||||||
|
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
|
||||||
|
|
||||||
|
interface TestPasswordEncoderInterface extends PasswordEncoderInterface
|
||||||
|
{
|
||||||
|
public function needsRehash(string $encoded): bool;
|
||||||
|
}
|
@ -15,6 +15,8 @@ use PHPUnit\Framework\TestCase;
|
|||||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||||
use Symfony\Component\Security\Core\User\ChainUserProvider;
|
use Symfony\Component\Security\Core\User\ChainUserProvider;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\User;
|
||||||
|
|
||||||
class ChainUserProviderTest extends TestCase
|
class ChainUserProviderTest extends TestCase
|
||||||
{
|
{
|
||||||
@ -188,6 +190,28 @@ class ChainUserProviderTest extends TestCase
|
|||||||
$this->assertSame($account, $provider->refreshUser($this->getAccount()));
|
$this->assertSame($account, $provider->refreshUser($this->getAccount()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testPasswordUpgrades()
|
||||||
|
{
|
||||||
|
$user = new User('user', 'pwd');
|
||||||
|
|
||||||
|
$provider1 = $this->getMockBuilder(PasswordUpgraderInterface::class)->getMock();
|
||||||
|
$provider1
|
||||||
|
->expects($this->once())
|
||||||
|
->method('upgradePassword')
|
||||||
|
->willThrowException(new UnsupportedUserException('unsupported'))
|
||||||
|
;
|
||||||
|
|
||||||
|
$provider2 = $this->getMockBuilder(PasswordUpgraderInterface::class)->getMock();
|
||||||
|
$provider2
|
||||||
|
->expects($this->once())
|
||||||
|
->method('upgradePassword')
|
||||||
|
->with($user, 'foobar')
|
||||||
|
;
|
||||||
|
|
||||||
|
$provider = new ChainUserProvider([$provider1, $provider2]);
|
||||||
|
$provider->upgradePassword($user, 'foobar');
|
||||||
|
}
|
||||||
|
|
||||||
protected function getAccount()
|
protected function getAccount()
|
||||||
{
|
{
|
||||||
return $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock();
|
return $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock();
|
||||||
|
@ -22,7 +22,7 @@ use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
|||||||
*
|
*
|
||||||
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||||
*/
|
*/
|
||||||
class ChainUserProvider implements UserProviderInterface
|
class ChainUserProvider implements UserProviderInterface, PasswordUpgraderInterface
|
||||||
{
|
{
|
||||||
private $providers;
|
private $providers;
|
||||||
|
|
||||||
@ -104,4 +104,20 @@ class ChainUserProvider implements UserProviderInterface
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
|
||||||
|
{
|
||||||
|
foreach ($this->providers as $provider) {
|
||||||
|
if ($provider instanceof PasswordUpgraderInterface) {
|
||||||
|
try {
|
||||||
|
$provider->upgradePassword($user, $newEncodedPassword);
|
||||||
|
} catch (UnsupportedUserException $e) {
|
||||||
|
// ignore: password upgrades are opportunistic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Security\Core\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Nicolas Grekas <p@tchwork.com>
|
||||||
|
*/
|
||||||
|
interface PasswordUpgraderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Upgrades the encoded password of a user, typically for using a better hash algorithm.
|
||||||
|
*
|
||||||
|
* This method should persist the new password in the user storage and update the $user object accordingly.
|
||||||
|
* Because you don't want your users not being able to log in, this method should be opportunistic:
|
||||||
|
* it's fine if it does nothing or if it fails without throwing any exception.
|
||||||
|
*/
|
||||||
|
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void;
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Security\Guard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional interface for "guard" authenticators that deal with user passwords.
|
||||||
|
*/
|
||||||
|
interface PasswordAuthenticatedInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns the clear-text password contained in credentials if any.
|
||||||
|
*
|
||||||
|
* @param mixed The user credentials
|
||||||
|
*/
|
||||||
|
public function getPassword($credentials): ?string;
|
||||||
|
}
|
@ -13,14 +13,17 @@ namespace Symfony\Component\Security\Guard\Provider;
|
|||||||
|
|
||||||
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
|
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||||
use Symfony\Component\Security\Core\Exception\AuthenticationExpiredException;
|
use Symfony\Component\Security\Core\Exception\AuthenticationExpiredException;
|
||||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||||
use Symfony\Component\Security\Guard\AuthenticatorInterface;
|
use Symfony\Component\Security\Guard\AuthenticatorInterface;
|
||||||
|
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
|
||||||
use Symfony\Component\Security\Guard\Token\GuardTokenInterface;
|
use Symfony\Component\Security\Guard\Token\GuardTokenInterface;
|
||||||
use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken;
|
use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken;
|
||||||
|
|
||||||
@ -39,19 +42,19 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface
|
|||||||
private $userProvider;
|
private $userProvider;
|
||||||
private $providerKey;
|
private $providerKey;
|
||||||
private $userChecker;
|
private $userChecker;
|
||||||
|
private $passwordEncoder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationListener
|
* @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationListener
|
||||||
* @param UserProviderInterface $userProvider The user provider
|
|
||||||
* @param string $providerKey The provider (i.e. firewall) key
|
* @param string $providerKey The provider (i.e. firewall) key
|
||||||
* @param UserCheckerInterface $userChecker
|
|
||||||
*/
|
*/
|
||||||
public function __construct($guardAuthenticators, UserProviderInterface $userProvider, string $providerKey, UserCheckerInterface $userChecker)
|
public function __construct($guardAuthenticators, UserProviderInterface $userProvider, string $providerKey, UserCheckerInterface $userChecker, UserPasswordEncoderInterface $passwordEncoder = null)
|
||||||
{
|
{
|
||||||
$this->guardAuthenticators = $guardAuthenticators;
|
$this->guardAuthenticators = $guardAuthenticators;
|
||||||
$this->userProvider = $userProvider;
|
$this->userProvider = $userProvider;
|
||||||
$this->providerKey = $providerKey;
|
$this->providerKey = $providerKey;
|
||||||
$this->userChecker = $userChecker;
|
$this->userChecker = $userChecker;
|
||||||
|
$this->passwordEncoder = $passwordEncoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -113,6 +116,9 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface
|
|||||||
if (true !== $guardAuthenticator->checkCredentials($token->getCredentials(), $user)) {
|
if (true !== $guardAuthenticator->checkCredentials($token->getCredentials(), $user)) {
|
||||||
throw new BadCredentialsException(sprintf('Authentication failed because %s::checkCredentials() did not return true.', \get_class($guardAuthenticator)));
|
throw new BadCredentialsException(sprintf('Authentication failed because %s::checkCredentials() did not return true.', \get_class($guardAuthenticator)));
|
||||||
}
|
}
|
||||||
|
if ($this->userProvider instanceof PasswordUpgraderInterface && $guardAuthenticator instanceof PasswordAuthenticatedInterface && null !== $this->passwordEncoder && (null !== $password = $guardAuthenticator->getPassword($token->getCredentials())) && method_exists($this->passwordEncoder, 'needsRehash') && $this->passwordEncoder->needsRehash($user)) {
|
||||||
|
$this->userProvider->upgradePassword($user, $this->passwordEncoder->encodePassword($user, $password));
|
||||||
|
}
|
||||||
$this->userChecker->checkPostAuth($user);
|
$this->userChecker->checkPostAuth($user);
|
||||||
|
|
||||||
// turn the UserInterface into a TokenInterface
|
// turn the UserInterface into a TokenInterface
|
||||||
|
Reference in New Issue
Block a user