From 30d5e82156c60ec00e745dad9768d2196adc14cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Deruss=C3=A9?= Date: Sat, 5 Dec 2020 21:52:04 +0100 Subject: [PATCH 01/35] Assert voter returns valid decision --- Authorization/AccessDecisionManager.php | 10 ++++++++ .../AccessDecisionManagerTest.php | 25 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/Authorization/AccessDecisionManager.php b/Authorization/AccessDecisionManager.php index 8356c38b..7c4cbd72 100644 --- a/Authorization/AccessDecisionManager.php +++ b/Authorization/AccessDecisionManager.php @@ -89,6 +89,8 @@ private function decideAffirmative(TokenInterface $token, array $attributes, $ob if (VoterInterface::ACCESS_DENIED === $result) { ++$deny; + } elseif (VoterInterface::ACCESS_ABSTAIN !== $result) { + trigger_deprecation('symfony/security-core', '5.3', 'Returning "%s" in "%s::vote()" is deprecated, return one of "%s" constants: "ACCESS_GRANTED", "ACCESS_DENIED" or "ACCESS_ABSTAIN".', var_export($result, true), get_debug_type($voter), VoterInterface::class); } } @@ -124,6 +126,8 @@ private function decideConsensus(TokenInterface $token, array $attributes, $obje ++$grant; } elseif (VoterInterface::ACCESS_DENIED === $result) { ++$deny; + } elseif (VoterInterface::ACCESS_ABSTAIN !== $result) { + trigger_deprecation('symfony/security-core', '5.3', 'Returning "%s" in "%s::vote()" is deprecated, return one of "%s" constants: "ACCESS_GRANTED", "ACCESS_DENIED" or "ACCESS_ABSTAIN".', var_export($result, true), get_debug_type($voter), VoterInterface::class); } } @@ -161,6 +165,8 @@ private function decideUnanimous(TokenInterface $token, array $attributes, $obje if (VoterInterface::ACCESS_GRANTED === $result) { ++$grant; + } elseif (VoterInterface::ACCESS_ABSTAIN !== $result) { + trigger_deprecation('symfony/security-core', '5.3', 'Returning "%s" in "%s::vote()" is deprecated, return one of "%s" constants: "ACCESS_GRANTED", "ACCESS_DENIED" or "ACCESS_ABSTAIN".', var_export($result, true), get_debug_type($voter), VoterInterface::class); } } } @@ -192,6 +198,10 @@ private function decidePriority(TokenInterface $token, array $attributes, $objec if (VoterInterface::ACCESS_DENIED === $result) { return false; } + + if (VoterInterface::ACCESS_ABSTAIN !== $result) { + trigger_deprecation('symfony/security-core', '5.3', 'Returning "%s" in "%s::vote()" is deprecated, return one of "%s" constants: "ACCESS_GRANTED", "ACCESS_DENIED" or "ACCESS_ABSTAIN".', var_export($result, true), get_debug_type($voter), VoterInterface::class); + } } return $this->allowIfAllAbstainDecisions; diff --git a/Tests/Authorization/AccessDecisionManagerTest.php b/Tests/Authorization/AccessDecisionManagerTest.php index 0e3c62c5..2f7ce5e9 100644 --- a/Tests/Authorization/AccessDecisionManagerTest.php +++ b/Tests/Authorization/AccessDecisionManagerTest.php @@ -12,11 +12,14 @@ namespace Symfony\Component\Security\Core\Tests\Authorization; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; class AccessDecisionManagerTest extends TestCase { + use ExpectDeprecationTrait; + public function testSetUnsupportedStrategy() { $this->expectException('InvalidArgumentException'); @@ -34,6 +37,20 @@ public function testStrategies($strategy, $voters, $allowIfAllAbstainDecisions, $this->assertSame($expected, $manager->decide($token, ['ROLE_FOO'])); } + /** + * @dataProvider provideStrategies + * @group legacy + */ + public function testDeprecatedVoter($strategy) + { + $token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock(); + $manager = new AccessDecisionManager([$this->getVoter(3)], $strategy); + + $this->expectDeprecation('Since symfony/security-core 5.3: Returning "3" in "%s::vote()" is deprecated, return one of "Symfony\Component\Security\Core\Authorization\Voter\VoterInterface" constants: "ACCESS_GRANTED", "ACCESS_DENIED" or "ACCESS_ABSTAIN".'); + + $manager->decide($token, ['ROLE_FOO']); + } + public function getStrategyTests() { return [ @@ -94,6 +111,14 @@ public function getStrategyTests() ]; } + public function provideStrategies() + { + yield [AccessDecisionManager::STRATEGY_AFFIRMATIVE]; + yield [AccessDecisionManager::STRATEGY_CONSENSUS]; + yield [AccessDecisionManager::STRATEGY_UNANIMOUS]; + yield [AccessDecisionManager::STRATEGY_PRIORITY]; + } + protected function getVoters($grants, $denies, $abstains) { $voters = []; From b79e48800b43302d9ea57ad08f7d2f554c4205f9 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 11 Jan 2021 12:03:44 +0100 Subject: [PATCH 02/35] Use ::class keyword when possible --- Tests/Authorization/AccessDecisionManagerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Authorization/AccessDecisionManagerTest.php b/Tests/Authorization/AccessDecisionManagerTest.php index ecf19e22..091748f0 100644 --- a/Tests/Authorization/AccessDecisionManagerTest.php +++ b/Tests/Authorization/AccessDecisionManagerTest.php @@ -43,7 +43,7 @@ public function testStrategies($strategy, $voters, $allowIfAllAbstainDecisions, */ public function testDeprecatedVoter($strategy) { - $token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock(); + $token = $this->getMockBuilder(\Symfony\Component\Security\Core\Authentication\Token\TokenInterface::class)->getMock(); $manager = new AccessDecisionManager([$this->getVoter(3)], $strategy); $this->expectDeprecation('Since symfony/security-core 5.3: Returning "3" in "%s::vote()" is deprecated, return one of "Symfony\Component\Security\Core\Authorization\Voter\VoterInterface" constants: "ACCESS_GRANTED", "ACCESS_DENIED" or "ACCESS_ABSTAIN".'); From 4cb15ee8463bc5e46979ccca922ce60800d8196d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Fri, 15 Jan 2021 18:40:08 +0100 Subject: [PATCH 03/35] [Security] RoleHierarchy returns unique an unique array of roles --- Role/RoleHierarchy.php | 2 +- Tests/Role/RoleHierarchyTest.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Role/RoleHierarchy.php b/Role/RoleHierarchy.php index d911fe3d..76a5548d 100644 --- a/Role/RoleHierarchy.php +++ b/Role/RoleHierarchy.php @@ -48,7 +48,7 @@ public function getReachableRoleNames(array $roles): array } } - return $reachableRoles; + return array_values(array_unique($reachableRoles)); } protected function buildRoleMap() diff --git a/Tests/Role/RoleHierarchyTest.php b/Tests/Role/RoleHierarchyTest.php index b84889f5..5c42e0b3 100644 --- a/Tests/Role/RoleHierarchyTest.php +++ b/Tests/Role/RoleHierarchyTest.php @@ -28,5 +28,6 @@ public function testGetReachableRoleNames() $this->assertEquals(['ROLE_ADMIN', 'ROLE_USER'], $role->getReachableRoleNames(['ROLE_ADMIN'])); $this->assertEquals(['ROLE_FOO', 'ROLE_ADMIN', 'ROLE_USER'], $role->getReachableRoleNames(['ROLE_FOO', 'ROLE_ADMIN'])); $this->assertEquals(['ROLE_SUPER_ADMIN', 'ROLE_ADMIN', 'ROLE_FOO', 'ROLE_USER'], $role->getReachableRoleNames(['ROLE_SUPER_ADMIN'])); + $this->assertEquals(['ROLE_SUPER_ADMIN', 'ROLE_ADMIN', 'ROLE_FOO', 'ROLE_USER'], $role->getReachableRoleNames(['ROLE_SUPER_ADMIN', 'ROLE_SUPER_ADMIN'])); } } From 9d7eb8a1a382e58a6d146959f98c18b7c2c1fd6d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 27 Jan 2021 12:34:01 +0100 Subject: [PATCH 04/35] More cleanups and fixes --- Tests/Authorization/AccessDecisionManagerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Authorization/AccessDecisionManagerTest.php b/Tests/Authorization/AccessDecisionManagerTest.php index e2c5ea0f..375fb6d6 100644 --- a/Tests/Authorization/AccessDecisionManagerTest.php +++ b/Tests/Authorization/AccessDecisionManagerTest.php @@ -44,7 +44,7 @@ public function testStrategies($strategy, $voters, $allowIfAllAbstainDecisions, */ public function testDeprecatedVoter($strategy) { - $token = $this->getMockBuilder(\Symfony\Component\Security\Core\Authentication\Token\TokenInterface::class)->getMock(); + $token = $this->createMock(TokenInterface::class); $manager = new AccessDecisionManager([$this->getVoter(3)], $strategy); $this->expectDeprecation('Since symfony/security-core 5.3: Returning "3" in "%s::vote()" is deprecated, return one of "Symfony\Component\Security\Core\Authorization\Voter\VoterInterface" constants: "ACCESS_GRANTED", "ACCESS_DENIED" or "ACCESS_ABSTAIN".'); From 01f978e1fe34e53123e3ffb1662bf165403b204d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Deruss=C3=A9?= Date: Sat, 17 Oct 2020 23:28:45 +0200 Subject: [PATCH 05/35] Deprecat service "session" --- .../Storage/UsageTrackingTokenStorage.php | 25 ++++++++++++++----- .../Storage/UsageTrackingTokenStorageTest.php | 11 ++++++-- composer.json | 3 ++- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/Authentication/Token/Storage/UsageTrackingTokenStorage.php b/Authentication/Token/Storage/UsageTrackingTokenStorage.php index b90d5ab2..0b8d9c32 100644 --- a/Authentication/Token/Storage/UsageTrackingTokenStorage.php +++ b/Authentication/Token/Storage/UsageTrackingTokenStorage.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Core\Authentication\Token\Storage; use Psr\Container\ContainerInterface; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface; @@ -24,13 +25,13 @@ final class UsageTrackingTokenStorage implements TokenStorageInterface, ServiceSubscriberInterface { private $storage; - private $sessionLocator; + private $container; private $enableUsageTracking = false; - public function __construct(TokenStorageInterface $storage, ContainerInterface $sessionLocator) + public function __construct(TokenStorageInterface $storage, ContainerInterface $container) { $this->storage = $storage; - $this->sessionLocator = $sessionLocator; + $this->container = $container; } /** @@ -40,7 +41,7 @@ public function getToken(): ?TokenInterface { if ($this->enableUsageTracking) { // increments the internal session usage index - $this->sessionLocator->get('session')->getMetadataBag(); + $this->getSession()->getMetadataBag(); } return $this->storage->getToken(); @@ -55,7 +56,7 @@ public function setToken(TokenInterface $token = null): void if ($token && $this->enableUsageTracking) { // increments the internal session usage index - $this->sessionLocator->get('session')->getMetadataBag(); + $this->getSession()->getMetadataBag(); } } @@ -72,7 +73,19 @@ public function disableUsageTracking(): void public static function getSubscribedServices(): array { return [ - 'session' => SessionInterface::class, + 'request_stack' => RequestStack::class, ]; } + + private function getSession(): SessionInterface + { + // BC for symfony/security-bundle < 5.3 + if ($this->container->has('session')) { + trigger_deprecation('symfony/security-core', '5.3', 'Injecting the "session" in "%s" is deprecated, inject the "request_stack" instead.', __CLASS__); + + return $this->container->get('session'); + } + + return $this->container->get('request_stack')->getSession(); + } } diff --git a/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php b/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php index 607ccc75..c5d2eaf5 100644 --- a/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php +++ b/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php @@ -13,6 +13,8 @@ use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage; @@ -24,14 +26,19 @@ class UsageTrackingTokenStorageTest extends TestCase public function testGetSetToken() { $sessionAccess = 0; - $sessionLocator = new class(['session' => function () use (&$sessionAccess) { + $sessionLocator = new class(['request_stack' => function () use (&$sessionAccess) { ++$sessionAccess; $session = $this->createMock(SessionInterface::class); $session->expects($this->once()) ->method('getMetadataBag'); - return $session; + $request = new Request(); + $request->setSession($session); + $requestStack = new RequestStack(); + $requestStack->push($request); + + return $requestStack; }]) implements ContainerInterface { use ServiceLocatorTrait; }; diff --git a/composer.json b/composer.json index 3d74c1c7..48a6a46e 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "psr/container": "^1.0", "symfony/event-dispatcher": "^4.4|^5.0", "symfony/expression-language": "^4.4|^5.0", - "symfony/http-foundation": "^4.4|^5.0", + "symfony/http-foundation": "^5.3", "symfony/ldap": "^4.4|^5.0", "symfony/translation": "^4.4|^5.0", "symfony/validator": "^5.2", @@ -34,6 +34,7 @@ }, "conflict": { "symfony/event-dispatcher": "<4.4", + "symfony/http-foundation": "<5.3", "symfony/security-guard": "<4.4", "symfony/ldap": "<4.4", "symfony/validator": "<5.2" From 589995352a5d12840f57a02c117614248d5274d9 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Tue, 5 Jan 2021 01:14:26 +0100 Subject: [PATCH 06/35] [Security] Extract password hashing from security-core - using the right naming --- .../AuthenticationProviderManager.php | 4 + .../Provider/DaoAuthenticationProvider.php | 37 +++++-- Encoder/BasePasswordEncoder.php | 6 ++ Encoder/EncoderAwareInterface.php | 4 + Encoder/EncoderFactory.php | 8 +- Encoder/EncoderFactoryInterface.php | 5 + Encoder/LegacyEncoderTrait.php | 56 +++++++++++ Encoder/MessageDigestPasswordEncoder.php | 58 ++--------- Encoder/MigratingPasswordEncoder.php | 9 +- Encoder/NativePasswordEncoder.php | 98 ++----------------- Encoder/PasswordEncoderInterface.php | 5 + Encoder/Pbkdf2PasswordEncoder.php | 55 ++--------- Encoder/PlaintextPasswordEncoder.php | 40 ++------ Encoder/SelfSaltingEncoderInterface.php | 6 ++ Encoder/SodiumPasswordEncoder.php | 94 ++---------------- Encoder/UserPasswordEncoder.php | 5 + Encoder/UserPasswordEncoderInterface.php | 5 + .../DaoAuthenticationProviderTest.php | 95 +++++++++++------- Tests/Encoder/EncoderFactoryTest.php | 28 ++++++ .../MessageDigestPasswordEncoderTest.php | 3 + .../Encoder/MigratingPasswordEncoderTest.php | 3 + Tests/Encoder/NativePasswordEncoderTest.php | 1 + Tests/Encoder/Pbkdf2PasswordEncoderTest.php | 3 + .../Encoder/PlaintextPasswordEncoderTest.php | 3 + Tests/Encoder/SodiumPasswordEncoderTest.php | 3 + .../Encoder/TestPasswordEncoderInterface.php | 3 + Tests/Encoder/UserPasswordEncoderTest.php | 3 + User/PasswordUpgraderInterface.php | 4 +- User/UserInterface.php | 12 +-- .../Constraints/UserPasswordValidator.php | 19 +++- composer.json | 3 +- 31 files changed, 306 insertions(+), 372 deletions(-) create mode 100644 Encoder/LegacyEncoderTrait.php diff --git a/Authentication/AuthenticationProviderManager.php b/Authentication/AuthenticationProviderManager.php index e91c5d81..c4099603 100644 --- a/Authentication/AuthenticationProviderManager.php +++ b/Authentication/AuthenticationProviderManager.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Security\Core\Authentication; +use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException; use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\AuthenticationEvents; @@ -18,6 +19,7 @@ use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; use Symfony\Component\Security\Core\Exception\AccountStatusException; use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\ProviderNotFoundException; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -89,6 +91,8 @@ public function authenticate(TokenInterface $token) break; } catch (AuthenticationException $e) { $lastException = $e; + } catch (InvalidPasswordException $e) { + $lastException = new BadCredentialsException('Bad credentials.', 0, $e); } } diff --git a/Authentication/Provider/DaoAuthenticationProvider.php b/Authentication/Provider/DaoAuthenticationProvider.php index c65a9505..26beb6b9 100644 --- a/Authentication/Provider/DaoAuthenticationProvider.php +++ b/Authentication/Provider/DaoAuthenticationProvider.php @@ -20,6 +20,7 @@ use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; /** * DaoAuthenticationProvider uses a UserProviderInterface to retrieve the user @@ -29,14 +30,21 @@ */ class DaoAuthenticationProvider extends UserAuthenticationProvider { - private $encoderFactory; + private $hasherFactory; private $userProvider; - public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, string $providerKey, EncoderFactoryInterface $encoderFactory, bool $hideUserNotFoundExceptions = true) + /** + * @param PasswordHasherFactoryInterface $hasherFactory + */ + public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, string $providerKey, $hasherFactory, bool $hideUserNotFoundExceptions = true) { parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions); - $this->encoderFactory = $encoderFactory; + if ($hasherFactory instanceof EncoderFactoryInterface) { + trigger_deprecation('symfony/security-core', '5.3', 'Passing a "%s" instance to the "%s" constructor is deprecated, use "%s" instead.', EncoderFactoryInterface::class, __CLASS__, PasswordHasherFactoryInterface::class); + } + + $this->hasherFactory = $hasherFactory; $this->userProvider = $userProvider; } @@ -59,14 +67,29 @@ protected function checkAuthentication(UserInterface $user, UsernamePasswordToke throw new BadCredentialsException('The presented password is invalid.'); } - $encoder = $this->encoderFactory->getEncoder($user); + // deprecated since Symfony 5.3 + if ($this->hasherFactory instanceof EncoderFactoryInterface) { + $encoder = $this->hasherFactory->getEncoder($user); + + if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) { + 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())); + } + + return; + } + + $hasher = $this->hasherFactory->getPasswordHasher($user); - if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) { + if (!$hasher->verify($user->getPassword(), $presentedPassword, $user->getSalt())) { 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())); + if ($this->userProvider instanceof PasswordUpgraderInterface && $hasher->needsRehash($user->getPassword())) { + $this->userProvider->upgradePassword($user, $hasher->hash($presentedPassword, $user->getSalt())); } } } diff --git a/Encoder/BasePasswordEncoder.php b/Encoder/BasePasswordEncoder.php index e067a48a..9c014d9e 100644 --- a/Encoder/BasePasswordEncoder.php +++ b/Encoder/BasePasswordEncoder.php @@ -11,10 +11,16 @@ namespace Symfony\Component\Security\Core\Encoder; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', BasePasswordEncoder::class, CheckPasswordLengthTrait::class)); + +use Symfony\Component\PasswordHasher\Hasher\CheckPasswordLengthTrait; + /** * BasePasswordEncoder is the base class for all password encoders. * * @author Fabien Potencier + * + * @deprecated since Symfony 5.3, use CheckPasswordLengthTrait instead */ abstract class BasePasswordEncoder implements PasswordEncoderInterface { diff --git a/Encoder/EncoderAwareInterface.php b/Encoder/EncoderAwareInterface.php index 546f4f73..70231e2c 100644 --- a/Encoder/EncoderAwareInterface.php +++ b/Encoder/EncoderAwareInterface.php @@ -11,8 +11,12 @@ namespace Symfony\Component\Security\Core\Encoder; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface; + /** * @author Christophe Coevoet + * + * @deprecated since Symfony 5.3, use {@link PasswordHasherAwareInterface} instead. */ interface EncoderAwareInterface { diff --git a/Encoder/EncoderFactory.php b/Encoder/EncoderFactory.php index d07891bf..e90498a3 100644 --- a/Encoder/EncoderFactory.php +++ b/Encoder/EncoderFactory.php @@ -11,12 +11,18 @@ namespace Symfony\Component\Security\Core\Encoder; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', EncoderFactory::class, PasswordHasherFactory::class)); + use Symfony\Component\Security\Core\Exception\LogicException; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory; /** * A generic encoder factory implementation. * * @author Johannes M. Schmitt + * + * @deprecated since Symfony 5.3, use {@link PasswordHasherFactory} instead */ class EncoderFactory implements EncoderFactoryInterface { @@ -34,7 +40,7 @@ public function getEncoder($user) { $encoderKey = null; - if ($user instanceof EncoderAwareInterface && (null !== $encoderName = $user->getEncoderName())) { + if (($user instanceof PasswordHasherAwareInterface && null !== $encoderName = $user->getPasswordHasherName()) || ($user instanceof EncoderAwareInterface && null !== $encoderName = $user->getEncoderName())) { if (!\array_key_exists($encoderName, $this->encoders)) { throw new \RuntimeException(sprintf('The encoder "%s" was not configured.', $encoderName)); } diff --git a/Encoder/EncoderFactoryInterface.php b/Encoder/EncoderFactoryInterface.php index 2b9834b6..65fd12d8 100644 --- a/Encoder/EncoderFactoryInterface.php +++ b/Encoder/EncoderFactoryInterface.php @@ -11,12 +11,17 @@ namespace Symfony\Component\Security\Core\Encoder; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', EncoderFactoryInterface::class, PasswordHasherFactoryInterface::class)); + use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; /** * EncoderFactoryInterface to support different encoders for different accounts. * * @author Johannes M. Schmitt + * + * @deprecated since Symfony 5.3, use {@link PasswordHasherFactoryInterface} instead */ interface EncoderFactoryInterface { diff --git a/Encoder/LegacyEncoderTrait.php b/Encoder/LegacyEncoderTrait.php new file mode 100644 index 00000000..d1263213 --- /dev/null +++ b/Encoder/LegacyEncoderTrait.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Encoder; + +use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException; +use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface; +use Symfony\Component\PasswordHasher\PasswordHasherInterface; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; + +/** + * @internal + */ +trait LegacyEncoderTrait +{ + /** + * @var PasswordHasherInterface|LegacyPasswordHasherInterface + */ + private $hasher; + + /** + * {@inheritdoc} + */ + public function encodePassword(string $raw, ?string $salt): string + { + try { + return $this->hasher->hash($raw, $salt); + } catch (InvalidPasswordException $e) { + throw new BadCredentialsException('Bad credentials.'); + } + } + + /** + * {@inheritdoc} + */ + public function isPasswordValid(string $encoded, string $raw, ?string $salt): bool + { + return $this->hasher->verify($encoded, $raw, $salt); + } + + /** + * {@inheritdoc} + */ + public function needsRehash(string $encoded): bool + { + return $this->hasher->needsRehash($encoded); + } +} diff --git a/Encoder/MessageDigestPasswordEncoder.php b/Encoder/MessageDigestPasswordEncoder.php index d769f2f4..d4b1fb54 100644 --- a/Encoder/MessageDigestPasswordEncoder.php +++ b/Encoder/MessageDigestPasswordEncoder.php @@ -11,19 +11,20 @@ namespace Symfony\Component\Security\Core\Encoder; -use Symfony\Component\Security\Core\Exception\BadCredentialsException; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', MessageDigestPasswordEncoder::class, MessageDigestPasswordHasher::class)); + +use Symfony\Component\PasswordHasher\Hasher\MessageDigestPasswordHasher; /** * MessageDigestPasswordEncoder uses a message digest algorithm. * * @author Fabien Potencier + * + * @deprecated since Symfony 5.3, use {@link MessageDigestPasswordHasher} instead */ class MessageDigestPasswordEncoder extends BasePasswordEncoder { - private $algorithm; - private $encodeHashAsBase64; - private $iterations = 1; - private $encodedLength = -1; + use LegacyEncoderTrait; /** * @param string $algorithm The digest algorithm to use @@ -32,51 +33,6 @@ class MessageDigestPasswordEncoder extends BasePasswordEncoder */ public function __construct(string $algorithm = 'sha512', bool $encodeHashAsBase64 = true, int $iterations = 5000) { - $this->algorithm = $algorithm; - $this->encodeHashAsBase64 = $encodeHashAsBase64; - - try { - $this->encodedLength = \strlen($this->encodePassword('', 'salt')); - } catch (\LogicException $e) { - // ignore algorithm not supported - } - - $this->iterations = $iterations; - } - - /** - * {@inheritdoc} - */ - public function encodePassword(string $raw, ?string $salt) - { - if ($this->isPasswordTooLong($raw)) { - throw new BadCredentialsException('Invalid password.'); - } - - if (!\in_array($this->algorithm, hash_algos(), true)) { - throw new \LogicException(sprintf('The algorithm "%s" is not supported.', $this->algorithm)); - } - - $salted = $this->mergePasswordAndSalt($raw, $salt); - $digest = hash($this->algorithm, $salted, true); - - // "stretch" hash - for ($i = 1; $i < $this->iterations; ++$i) { - $digest = hash($this->algorithm, $digest.$salted, true); - } - - return $this->encodeHashAsBase64 ? base64_encode($digest) : bin2hex($digest); - } - - /** - * {@inheritdoc} - */ - public function isPasswordValid(string $encoded, string $raw, ?string $salt) - { - if (\strlen($encoded) !== $this->encodedLength || false !== strpos($encoded, '$')) { - return false; - } - - return !$this->isPasswordTooLong($raw) && $this->comparePasswords($encoded, $this->encodePassword($raw, $salt)); + $this->hasher = new MessageDigestPasswordHasher($algorithm, $encodeHashAsBase64, $iterations); } } diff --git a/Encoder/MigratingPasswordEncoder.php b/Encoder/MigratingPasswordEncoder.php index cd10b32b..be178731 100644 --- a/Encoder/MigratingPasswordEncoder.php +++ b/Encoder/MigratingPasswordEncoder.php @@ -11,6 +11,10 @@ namespace Symfony\Component\Security\Core\Encoder; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', MigratingPasswordEncoder::class, MigratingPasswordHasher::class)); + +use Symfony\Component\PasswordHasher\Hasher\MigratingPasswordHasher; + /** * Hashes passwords using the best available encoder. * Validates them using a chain of encoders. @@ -19,12 +23,11 @@ * could be used to authenticate successfully without knowing the cleartext password. * * @author Nicolas Grekas + * + * @deprecated since Symfony 5.3, use {@link MigratingPasswordHasher} instead */ final class MigratingPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface { - private $bestEncoder; - private $extraEncoders; - public function __construct(PasswordEncoderInterface $bestEncoder, PasswordEncoderInterface ...$extraEncoders) { $this->bestEncoder = $bestEncoder; diff --git a/Encoder/NativePasswordEncoder.php b/Encoder/NativePasswordEncoder.php index 83b7f3f1..b3bd4b54 100644 --- a/Encoder/NativePasswordEncoder.php +++ b/Encoder/NativePasswordEncoder.php @@ -11,7 +11,10 @@ namespace Symfony\Component\Security\Core\Encoder; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', NativePasswordEncoder::class, NativePasswordHasher::class)); + use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher; /** * Hashes passwords using password_hash(). @@ -19,105 +22,18 @@ * @author Elnur Abdurrakhimov * @author Terje Bråten * @author Nicolas Grekas + * + * @deprecated since Symfony 5.3, use {@link NativePasswordHasher} instead */ final class NativePasswordEncoder implements PasswordEncoderInterface, SelfSaltingEncoderInterface { - private const MAX_PASSWORD_LENGTH = 4096; - - private $algo = \PASSWORD_BCRYPT; - private $options; + use LegacyEncoderTrait; /** * @param string|null $algo An algorithm supported by password_hash() or null to use the stronger available algorithm */ public function __construct(int $opsLimit = null, int $memLimit = null, int $cost = null, string $algo = null) { - $cost = $cost ?? 13; - $opsLimit = $opsLimit ?? max(4, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE : 4); - $memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 1024); - - if (3 > $opsLimit) { - throw new \InvalidArgumentException('$opsLimit must be 3 or greater.'); - } - - if (10 * 1024 > $memLimit) { - throw new \InvalidArgumentException('$memLimit must be 10k or greater.'); - } - - if ($cost < 4 || 31 < $cost) { - throw new \InvalidArgumentException('$cost must be in the range of 4-31.'); - } - - $algos = [1 => \PASSWORD_BCRYPT, '2y' => \PASSWORD_BCRYPT]; - - if (\defined('PASSWORD_ARGON2I')) { - $this->algo = $algos[2] = $algos['argon2i'] = (string) \PASSWORD_ARGON2I; - } - - if (\defined('PASSWORD_ARGON2ID')) { - $this->algo = $algos[3] = $algos['argon2id'] = (string) \PASSWORD_ARGON2ID; - } - - if (null !== $algo) { - $this->algo = $algos[$algo] ?? $algo; - } - - $this->options = [ - 'cost' => $cost, - 'time_cost' => $opsLimit, - 'memory_cost' => $memLimit >> 10, - 'threads' => 1, - ]; - } - - /** - * {@inheritdoc} - */ - public function encodePassword(string $raw, ?string $salt): string - { - if (\strlen($raw) > self::MAX_PASSWORD_LENGTH || ((string) \PASSWORD_BCRYPT === $this->algo && 72 < \strlen($raw))) { - throw new BadCredentialsException('Invalid password.'); - } - - // Ignore $salt, the auto-generated one is always the best - - return password_hash($raw, $this->algo, $this->options); - } - - /** - * {@inheritdoc} - */ - public function isPasswordValid(string $encoded, string $raw, ?string $salt): bool - { - if ('' === $raw) { - return false; - } - - if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) { - return false; - } - - if (0 !== strpos($encoded, '$argon')) { - // BCrypt encodes only the first 72 chars - return (72 >= \strlen($raw) || 0 !== strpos($encoded, '$2')) && password_verify($raw, $encoded); - } - - if (\extension_loaded('sodium') && version_compare(\SODIUM_LIBRARY_VERSION, '1.0.14', '>=')) { - return sodium_crypto_pwhash_str_verify($encoded, $raw); - } - - if (\extension_loaded('libsodium') && version_compare(phpversion('libsodium'), '1.0.14', '>=')) { - return \Sodium\crypto_pwhash_str_verify($encoded, $raw); - } - - return password_verify($raw, $encoded); - } - - /** - * {@inheritdoc} - */ - public function needsRehash(string $encoded): bool - { - return password_needs_rehash($encoded, $this->algo, $this->options); + $this->hasher = new NativePasswordHasher($opsLimit, $memLimit, $cost, $algo); } } diff --git a/Encoder/PasswordEncoderInterface.php b/Encoder/PasswordEncoderInterface.php index 9d8d48f8..ba9216eb 100644 --- a/Encoder/PasswordEncoderInterface.php +++ b/Encoder/PasswordEncoderInterface.php @@ -11,12 +11,17 @@ namespace Symfony\Component\Security\Core\Encoder; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', PasswordEncoderInterface::class, PasswordHasherInterface::class)); + use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\PasswordHasher\PasswordHasherInterface; /** * PasswordEncoderInterface is the interface for all encoders. * * @author Fabien Potencier + * + * @deprecated since Symfony 5.3, use {@link PasswordHasherInterface} instead */ interface PasswordEncoderInterface { diff --git a/Encoder/Pbkdf2PasswordEncoder.php b/Encoder/Pbkdf2PasswordEncoder.php index ab5e1a53..a50ad01e 100644 --- a/Encoder/Pbkdf2PasswordEncoder.php +++ b/Encoder/Pbkdf2PasswordEncoder.php @@ -11,7 +11,10 @@ namespace Symfony\Component\Security\Core\Encoder; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', Pbkdf2PasswordEncoder::class, Pbkdf2PasswordHasher::class)); + use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher; /** * Pbkdf2PasswordEncoder uses the PBKDF2 (Password-Based Key Derivation Function 2). @@ -25,14 +28,12 @@ * @author Sebastiaan Stok * @author Andrew Johnson * @author Fabien Potencier + * + * @deprecated since Symfony 5.3, use {@link Pbkdf2PasswordHasher} instead */ class Pbkdf2PasswordEncoder extends BasePasswordEncoder { - private $algorithm; - private $encodeHashAsBase64; - private $iterations = 1; - private $length; - private $encodedLength = -1; + use LegacyEncoderTrait; /** * @param string $algorithm The digest algorithm to use @@ -42,48 +43,6 @@ class Pbkdf2PasswordEncoder extends BasePasswordEncoder */ public function __construct(string $algorithm = 'sha512', bool $encodeHashAsBase64 = true, int $iterations = 1000, int $length = 40) { - $this->algorithm = $algorithm; - $this->encodeHashAsBase64 = $encodeHashAsBase64; - $this->length = $length; - - try { - $this->encodedLength = \strlen($this->encodePassword('', 'salt')); - } catch (\LogicException $e) { - // ignore algorithm not supported - } - - $this->iterations = $iterations; - } - - /** - * {@inheritdoc} - * - * @throws \LogicException when the algorithm is not supported - */ - public function encodePassword(string $raw, ?string $salt) - { - if ($this->isPasswordTooLong($raw)) { - throw new BadCredentialsException('Invalid password.'); - } - - if (!\in_array($this->algorithm, hash_algos(), true)) { - throw new \LogicException(sprintf('The algorithm "%s" is not supported.', $this->algorithm)); - } - - $digest = hash_pbkdf2($this->algorithm, $raw, $salt, $this->iterations, $this->length, true); - - return $this->encodeHashAsBase64 ? base64_encode($digest) : bin2hex($digest); - } - - /** - * {@inheritdoc} - */ - public function isPasswordValid(string $encoded, string $raw, ?string $salt) - { - if (\strlen($encoded) !== $this->encodedLength || false !== strpos($encoded, '$')) { - return false; - } - - return !$this->isPasswordTooLong($raw) && $this->comparePasswords($encoded, $this->encodePassword($raw, $salt)); + $this->hasher = new Pbkdf2PasswordHasher($algorithm, $encodeHashAsBase64, $iterations, $length); } } diff --git a/Encoder/PlaintextPasswordEncoder.php b/Encoder/PlaintextPasswordEncoder.php index 90e7e3d5..65fc8502 100644 --- a/Encoder/PlaintextPasswordEncoder.php +++ b/Encoder/PlaintextPasswordEncoder.php @@ -11,7 +11,9 @@ namespace Symfony\Component\Security\Core\Encoder; -use Symfony\Component\Security\Core\Exception\BadCredentialsException; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', PlaintextPasswordEncoder::class, PlaintextPasswordHasher::class)); + +use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher; /** * PlaintextPasswordEncoder does not do any encoding but is useful in testing environments. @@ -19,46 +21,18 @@ * As this encoder is not cryptographically secure, usage of it in production environments is discouraged. * * @author Fabien Potencier + * + * @deprecated since Symfony 5.3, use {@link PlaintextPasswordHasher} instead */ class PlaintextPasswordEncoder extends BasePasswordEncoder { - private $ignorePasswordCase; + use LegacyEncoderTrait; /** * @param bool $ignorePasswordCase Compare password case-insensitive */ public function __construct(bool $ignorePasswordCase = false) { - $this->ignorePasswordCase = $ignorePasswordCase; - } - - /** - * {@inheritdoc} - */ - public function encodePassword(string $raw, ?string $salt) - { - if ($this->isPasswordTooLong($raw)) { - throw new BadCredentialsException('Invalid password.'); - } - - return $this->mergePasswordAndSalt($raw, $salt); - } - - /** - * {@inheritdoc} - */ - public function isPasswordValid(string $encoded, string $raw, ?string $salt) - { - if ($this->isPasswordTooLong($raw)) { - return false; - } - - $pass2 = $this->mergePasswordAndSalt($raw, $salt); - - if (!$this->ignorePasswordCase) { - return $this->comparePasswords($encoded, $pass2); - } - - return $this->comparePasswords(strtolower($encoded), strtolower($pass2)); + $this->hasher = new PlaintextPasswordHasher($ignorePasswordCase); } } diff --git a/Encoder/SelfSaltingEncoderInterface.php b/Encoder/SelfSaltingEncoderInterface.php index 37855b60..6bb983dd 100644 --- a/Encoder/SelfSaltingEncoderInterface.php +++ b/Encoder/SelfSaltingEncoderInterface.php @@ -11,11 +11,17 @@ namespace Symfony\Component\Security\Core\Encoder; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" interface is deprecated, use "%s" on hasher implementations that deal with salts instead.', SelfSaltingEncoderInterface::class, LegacyPasswordHasherInterface::class)); + +use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface; + /** * SelfSaltingEncoderInterface is a marker interface for encoders that do not * require a user-generated salt. * * @author Zan Baldwin + * + * @deprecated since Symfony 5.3, use {@link LegacyPasswordHasherInterface} instead */ interface SelfSaltingEncoderInterface { diff --git a/Encoder/SodiumPasswordEncoder.php b/Encoder/SodiumPasswordEncoder.php index 53c66600..480adb4a 100644 --- a/Encoder/SodiumPasswordEncoder.php +++ b/Encoder/SodiumPasswordEncoder.php @@ -11,8 +11,9 @@ namespace Symfony\Component\Security\Core\Encoder; -use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\Exception\LogicException; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', SodiumPasswordEncoder::class, SodiumPasswordHasher::class)); + +use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher; /** * Hashes passwords using libsodium. @@ -20,99 +21,20 @@ * @author Robin Chalas * @author Zan Baldwin * @author Dominik Müller + * + * @deprecated since Symfony 5.3, use {@link SodiumPasswordHasher} instead */ final class SodiumPasswordEncoder implements PasswordEncoderInterface, SelfSaltingEncoderInterface { - private const MAX_PASSWORD_LENGTH = 4096; - - private $opsLimit; - private $memLimit; + use LegacyEncoderTrait; public function __construct(int $opsLimit = null, int $memLimit = null) { - if (!self::isSupported()) { - throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.'); - } - - $this->opsLimit = $opsLimit ?? max(4, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE : 4); - $this->memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 1024); - - if (3 > $this->opsLimit) { - throw new \InvalidArgumentException('$opsLimit must be 3 or greater.'); - } - - if (10 * 1024 > $this->memLimit) { - throw new \InvalidArgumentException('$memLimit must be 10k or greater.'); - } + $this->hasher = new SodiumPasswordHasher($opsLimit, $memLimit); } public static function isSupported(): bool { - return version_compare(\extension_loaded('sodium') ? \SODIUM_LIBRARY_VERSION : phpversion('libsodium'), '1.0.14', '>='); - } - - /** - * {@inheritdoc} - */ - public function encodePassword(string $raw, ?string $salt): string - { - if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) { - throw new BadCredentialsException('Invalid password.'); - } - - if (\function_exists('sodium_crypto_pwhash_str')) { - return sodium_crypto_pwhash_str($raw, $this->opsLimit, $this->memLimit); - } - - if (\extension_loaded('libsodium')) { - return \Sodium\crypto_pwhash_str($raw, $this->opsLimit, $this->memLimit); - } - - throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.'); - } - - /** - * {@inheritdoc} - */ - public function isPasswordValid(string $encoded, string $raw, ?string $salt): bool - { - if ('' === $raw) { - return false; - } - - if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) { - return false; - } - - if (0 !== strpos($encoded, '$argon')) { - // Accept validating non-argon passwords for seamless migrations - return (72 >= \strlen($raw) || 0 !== strpos($encoded, '$2')) && password_verify($raw, $encoded); - } - - if (\function_exists('sodium_crypto_pwhash_str_verify')) { - return sodium_crypto_pwhash_str_verify($encoded, $raw); - } - - if (\extension_loaded('libsodium')) { - return \Sodium\crypto_pwhash_str_verify($encoded, $raw); - } - - return false; - } - - /** - * {@inheritdoc} - */ - public function needsRehash(string $encoded): bool - { - if (\function_exists('sodium_crypto_pwhash_str_needs_rehash')) { - return sodium_crypto_pwhash_str_needs_rehash($encoded, $this->opsLimit, $this->memLimit); - } - - if (\extension_loaded('libsodium')) { - return \Sodium\crypto_pwhash_str_needs_rehash($encoded, $this->opsLimit, $this->memLimit); - } - - throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.'); + return SodiumPasswordHasher::isSupported(); } } diff --git a/Encoder/UserPasswordEncoder.php b/Encoder/UserPasswordEncoder.php index aeb29956..bfe31a4a 100644 --- a/Encoder/UserPasswordEncoder.php +++ b/Encoder/UserPasswordEncoder.php @@ -11,12 +11,17 @@ namespace Symfony\Component\Security\Core\Encoder; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', UserPasswordEncoder::class, UserPasswordHasher::class)); + use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher; /** * A generic password encoder. * * @author Ariel Ferrandini + * + * @deprecated since Symfony 5.3, use {@link UserPasswordHasher} instead */ class UserPasswordEncoder implements UserPasswordEncoderInterface { diff --git a/Encoder/UserPasswordEncoderInterface.php b/Encoder/UserPasswordEncoderInterface.php index 522ec0b0..858e8367 100644 --- a/Encoder/UserPasswordEncoderInterface.php +++ b/Encoder/UserPasswordEncoderInterface.php @@ -11,12 +11,17 @@ namespace Symfony\Component\Security\Core\Encoder; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" interface is deprecated, use "%s" on hasher implementations that deal with salts instead.', UserPasswordEncoderInterface::class, UserPasswordHasherInterface::class)); + use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; /** * UserPasswordEncoderInterface is the interface for the password encoder service. * * @author Ariel Ferrandini + * + * @deprecated since Symfony 5.3, use {@link UserPasswordHasherInterface} instead */ interface UserPasswordEncoderInterface { diff --git a/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php b/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php index 57ed2d0b..20e75b80 100644 --- a/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php @@ -15,17 +15,17 @@ use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; -use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; -use Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder; use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; 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\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; +use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher; +use Symfony\Component\PasswordHasher\PasswordHasherInterface; class DaoAuthenticationProviderTest extends TestCase { @@ -39,7 +39,10 @@ public function testRetrieveUserWhenProviderDoesNotReturnAnUserInterface() $method->invoke($provider, 'fabien', $this->getSupportedToken()); } - public function testRetrieveUserWhenUsernameIsNotFound() + /** + * @group legacy + */ + public function testRetrieveUserWhenUsernameIsNotFoundWithLegacyEncoderFactory() { $this->expectException(UsernameNotFoundException::class); $userProvider = $this->createMock(UserProviderInterface::class); @@ -55,6 +58,22 @@ public function testRetrieveUserWhenUsernameIsNotFound() $method->invoke($provider, 'fabien', $this->getSupportedToken()); } + public function testRetrieveUserWhenUsernameIsNotFound() + { + $this->expectException(UsernameNotFoundException::class); + $userProvider = $this->createMock(UserProviderInterface::class); + $userProvider->expects($this->once()) + ->method('loadUserByUsername') + ->willThrowException(new UsernameNotFoundException()) + ; + + $provider = new DaoAuthenticationProvider($userProvider, $this->createMock(UserCheckerInterface::class), 'key', $this->createMock(PasswordHasherFactoryInterface::class)); + $method = new \ReflectionMethod($provider, 'retrieveUser'); + $method->setAccessible(true); + + $method->invoke($provider, 'fabien', $this->getSupportedToken()); + } + public function testRetrieveUserWhenAnExceptionOccurs() { $this->expectException(AuthenticationServiceException::class); @@ -64,7 +83,7 @@ public function testRetrieveUserWhenAnExceptionOccurs() ->willThrowException(new \RuntimeException()) ; - $provider = new DaoAuthenticationProvider($userProvider, $this->createMock(UserCheckerInterface::class), 'key', $this->createMock(EncoderFactoryInterface::class)); + $provider = new DaoAuthenticationProvider($userProvider, $this->createMock(UserCheckerInterface::class), 'key', $this->createMock(PasswordHasherFactoryInterface::class)); $method = new \ReflectionMethod($provider, 'retrieveUser'); $method->setAccessible(true); @@ -85,7 +104,7 @@ public function testRetrieveUserReturnsUserFromTokenOnReauthentication() ->willReturn($user) ; - $provider = new DaoAuthenticationProvider($userProvider, $this->createMock(UserCheckerInterface::class), 'key', $this->createMock(EncoderFactoryInterface::class)); + $provider = new DaoAuthenticationProvider($userProvider, $this->createMock(UserCheckerInterface::class), 'key', $this->createMock(PasswordHasherFactoryInterface::class)); $reflection = new \ReflectionMethod($provider, 'retrieveUser'); $reflection->setAccessible(true); $result = $reflection->invoke($provider, 'someUser', $token); @@ -103,7 +122,7 @@ public function testRetrieveUser() ->willReturn($user) ; - $provider = new DaoAuthenticationProvider($userProvider, $this->createMock(UserCheckerInterface::class), 'key', $this->createMock(EncoderFactoryInterface::class)); + $provider = new DaoAuthenticationProvider($userProvider, $this->createMock(UserCheckerInterface::class), 'key', $this->createMock(PasswordHasherFactoryInterface::class)); $method = new \ReflectionMethod($provider, 'retrieveUser'); $method->setAccessible(true); @@ -113,13 +132,13 @@ public function testRetrieveUser() public function testCheckAuthenticationWhenCredentialsAreEmpty() { $this->expectException(BadCredentialsException::class); - $encoder = $this->createMock(PasswordEncoderInterface::class); - $encoder + $hasher = $this->getMockBuilder(PasswordHasherInterface::class)->getMock(); + $hasher ->expects($this->never()) - ->method('isPasswordValid') + ->method('verify') ; - $provider = $this->getProvider(null, null, $encoder); + $provider = $this->getProvider(null, null, $hasher); $method = new \ReflectionMethod($provider, 'checkAuthentication'); $method->setAccessible(true); @@ -135,14 +154,14 @@ public function testCheckAuthenticationWhenCredentialsAreEmpty() public function testCheckAuthenticationWhenCredentialsAre0() { - $encoder = $this->createMock(PasswordEncoderInterface::class); - $encoder + $hasher = $this->createMock(PasswordHasherInterface::class); + $hasher ->expects($this->once()) - ->method('isPasswordValid') + ->method('verify') ->willReturn(true) ; - $provider = $this->getProvider(null, null, $encoder); + $provider = $this->getProvider(null, null, $hasher); $method = new \ReflectionMethod($provider, 'checkAuthentication'); $method->setAccessible(true); @@ -163,13 +182,13 @@ public function testCheckAuthenticationWhenCredentialsAre0() public function testCheckAuthenticationWhenCredentialsAreNotValid() { $this->expectException(BadCredentialsException::class); - $encoder = $this->createMock(PasswordEncoderInterface::class); - $encoder->expects($this->once()) - ->method('isPasswordValid') + $hasher = $this->createMock(PasswordHasherInterface::class); + $hasher->expects($this->once()) + ->method('verify') ->willReturn(false) ; - $provider = $this->getProvider(null, null, $encoder); + $provider = $this->getProvider(null, null, $hasher); $method = new \ReflectionMethod($provider, 'checkAuthentication'); $method->setAccessible(true); @@ -235,13 +254,13 @@ public function testCheckAuthenticationWhenTokenNeedsReauthenticationWorksWithou public function testCheckAuthentication() { - $encoder = $this->createMock(PasswordEncoderInterface::class); - $encoder->expects($this->once()) - ->method('isPasswordValid') + $hasher = $this->createMock(PasswordHasherInterface::class); + $hasher->expects($this->once()) + ->method('verify') ->willReturn(true) ; - $provider = $this->getProvider(null, null, $encoder); + $provider = $this->getProvider(null, null, $hasher); $method = new \ReflectionMethod($provider, 'checkAuthentication'); $method->setAccessible(true); @@ -258,21 +277,21 @@ public function testPasswordUpgrades() { $user = new User('user', 'pwd'); - $encoder = $this->createMock(TestPasswordEncoderInterface::class); - $encoder->expects($this->once()) - ->method('isPasswordValid') + $hasher = $this->createMock(PasswordHasherInterface::class); + $hasher->expects($this->once()) + ->method('verify') ->willReturn(true) ; - $encoder->expects($this->once()) - ->method('encodePassword') + $hasher->expects($this->once()) + ->method('hash') ->willReturn('foobar') ; - $encoder->expects($this->once()) + $hasher->expects($this->once()) ->method('needsRehash') ->willReturn(true) ; - $provider = $this->getProvider(null, null, $encoder); + $provider = $this->getProvider(null, null, $hasher); $userProvider = ((array) $provider)[sprintf("\0%s\0userProvider", DaoAuthenticationProvider::class)]; $userProvider->expects($this->once()) @@ -304,7 +323,7 @@ protected function getSupportedToken() return $mock; } - protected function getProvider($user = null, $userChecker = null, $passwordEncoder = null) + protected function getProvider($user = null, $userChecker = null, $passwordHasher = null) { $userProvider = $this->createMock(PasswordUpgraderProvider::class); if (null !== $user) { @@ -318,18 +337,18 @@ protected function getProvider($user = null, $userChecker = null, $passwordEncod $userChecker = $this->createMock(UserCheckerInterface::class); } - if (null === $passwordEncoder) { - $passwordEncoder = new PlaintextPasswordEncoder(); + if (null === $passwordHasher) { + $passwordHasher = new PlaintextPasswordHasher(); } - $encoderFactory = $this->createMock(EncoderFactoryInterface::class); - $encoderFactory + $hasherFactory = $this->createMock(PasswordHasherFactoryInterface::class); + $hasherFactory ->expects($this->any()) - ->method('getEncoder') - ->willReturn($passwordEncoder) + ->method('getPasswordHasher') + ->willReturn($passwordHasher) ; - return new DaoAuthenticationProvider($userProvider, $userChecker, 'key', $encoderFactory); + return new DaoAuthenticationProvider($userProvider, $userChecker, 'key', $hasherFactory); } } diff --git a/Tests/Encoder/EncoderFactoryTest.php b/Tests/Encoder/EncoderFactoryTest.php index a6999991..7b79986b 100644 --- a/Tests/Encoder/EncoderFactoryTest.php +++ b/Tests/Encoder/EncoderFactoryTest.php @@ -20,7 +20,13 @@ use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory; +use Symfony\Component\PasswordHasher\Hasher\MessageDigestPasswordHasher; +/** + * @group legacy + */ class EncoderFactoryTest extends TestCase { public function testGetEncoderWithMessageDigestEncoder() @@ -176,6 +182,17 @@ public function testDefaultMigratingEncoders() (new EncoderFactory([SomeUser::class => ['class' => SodiumPasswordEncoder::class, 'arguments' => []]]))->getEncoder(SomeUser::class) ); } + + public function testHasherAwareCompat() + { + $factory = new PasswordHasherFactory([ + 'encoder_name' => new MessageDigestPasswordHasher('sha1'), + ]); + + $encoder = $factory->getPasswordHasher(new HasherAwareUser('user', 'pass')); + $expectedEncoder = new MessageDigestPasswordHasher('sha1'); + $this->assertEquals($expectedEncoder->hash('foo', ''), $encoder->hash('foo', '')); + } } class SomeUser implements UserInterface @@ -214,3 +231,14 @@ public function getEncoderName(): ?string return $this->encoderName; } } + + +class HasherAwareUser extends SomeUser implements PasswordHasherAwareInterface +{ + public $hasherName = 'encoder_name'; + + public function getPasswordHasherName(): ?string + { + return $this->hasherName; + } +} diff --git a/Tests/Encoder/MessageDigestPasswordEncoderTest.php b/Tests/Encoder/MessageDigestPasswordEncoderTest.php index c2b514bb..a354b0db 100644 --- a/Tests/Encoder/MessageDigestPasswordEncoderTest.php +++ b/Tests/Encoder/MessageDigestPasswordEncoderTest.php @@ -15,6 +15,9 @@ use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder; use Symfony\Component\Security\Core\Exception\BadCredentialsException; +/** + * @group legacy + */ class MessageDigestPasswordEncoderTest extends TestCase { public function testIsPasswordValid() diff --git a/Tests/Encoder/MigratingPasswordEncoderTest.php b/Tests/Encoder/MigratingPasswordEncoderTest.php index efa360ec..fbaf89b0 100644 --- a/Tests/Encoder/MigratingPasswordEncoderTest.php +++ b/Tests/Encoder/MigratingPasswordEncoderTest.php @@ -15,6 +15,9 @@ use Symfony\Component\Security\Core\Encoder\MigratingPasswordEncoder; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; +/** + * @group legacy + */ class MigratingPasswordEncoderTest extends TestCase { public function testValidation() diff --git a/Tests/Encoder/NativePasswordEncoderTest.php b/Tests/Encoder/NativePasswordEncoderTest.php index c67bf866..9d864dfc 100644 --- a/Tests/Encoder/NativePasswordEncoderTest.php +++ b/Tests/Encoder/NativePasswordEncoderTest.php @@ -16,6 +16,7 @@ /** * @author Elnur Abdurrakhimov + * @group legacy */ class NativePasswordEncoderTest extends TestCase { diff --git a/Tests/Encoder/Pbkdf2PasswordEncoderTest.php b/Tests/Encoder/Pbkdf2PasswordEncoderTest.php index db274716..000e07d6 100644 --- a/Tests/Encoder/Pbkdf2PasswordEncoderTest.php +++ b/Tests/Encoder/Pbkdf2PasswordEncoderTest.php @@ -15,6 +15,9 @@ use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder; use Symfony\Component\Security\Core\Exception\BadCredentialsException; +/** + * @group legacy + */ class Pbkdf2PasswordEncoderTest extends TestCase { public function testIsPasswordValid() diff --git a/Tests/Encoder/PlaintextPasswordEncoderTest.php b/Tests/Encoder/PlaintextPasswordEncoderTest.php index fb5e6745..39804403 100644 --- a/Tests/Encoder/PlaintextPasswordEncoderTest.php +++ b/Tests/Encoder/PlaintextPasswordEncoderTest.php @@ -15,6 +15,9 @@ use Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder; use Symfony\Component\Security\Core\Exception\BadCredentialsException; +/** + * @group legacy + */ class PlaintextPasswordEncoderTest extends TestCase { public function testIsPasswordValid() diff --git a/Tests/Encoder/SodiumPasswordEncoderTest.php b/Tests/Encoder/SodiumPasswordEncoderTest.php index b4073a1c..4bae5f89 100644 --- a/Tests/Encoder/SodiumPasswordEncoderTest.php +++ b/Tests/Encoder/SodiumPasswordEncoderTest.php @@ -15,6 +15,9 @@ use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; use Symfony\Component\Security\Core\Exception\BadCredentialsException; +/** + * @group legacy + */ class SodiumPasswordEncoderTest extends TestCase { protected function setUp(): void diff --git a/Tests/Encoder/TestPasswordEncoderInterface.php b/Tests/Encoder/TestPasswordEncoderInterface.php index 13e2d0d3..3764038e 100644 --- a/Tests/Encoder/TestPasswordEncoderInterface.php +++ b/Tests/Encoder/TestPasswordEncoderInterface.php @@ -13,6 +13,9 @@ use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; +/** + * @group legacy + */ interface TestPasswordEncoderInterface extends PasswordEncoderInterface { public function needsRehash(string $encoded): bool; diff --git a/Tests/Encoder/UserPasswordEncoderTest.php b/Tests/Encoder/UserPasswordEncoderTest.php index 0d72919a..6f52fbf1 100644 --- a/Tests/Encoder/UserPasswordEncoderTest.php +++ b/Tests/Encoder/UserPasswordEncoderTest.php @@ -19,6 +19,9 @@ use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserInterface; +/** + * @group legacy + */ class UserPasswordEncoderTest extends TestCase { public function testEncodePassword() diff --git a/User/PasswordUpgraderInterface.php b/User/PasswordUpgraderInterface.php index 9c65298b..ef62023d 100644 --- a/User/PasswordUpgraderInterface.php +++ b/User/PasswordUpgraderInterface.php @@ -17,11 +17,11 @@ interface PasswordUpgraderInterface { /** - * Upgrades the encoded password of a user, typically for using a better hash algorithm. + * Upgrades the hashed 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; + public function upgradePassword(UserInterface $user, string $newHashedPassword): void; } diff --git a/User/UserInterface.php b/User/UserInterface.php index 239eb0ed..c005e3ca 100644 --- a/User/UserInterface.php +++ b/User/UserInterface.php @@ -15,7 +15,7 @@ * Represents the interface that all user classes must implement. * * This interface is useful because the authentication layer can deal with - * the object through its lifecycle, using the object to get the encoded + * the object through its lifecycle, using the object to get the hashed * password (for checking against a submitted password), assigning roles * and so on. * @@ -49,17 +49,17 @@ public function getRoles(); /** * Returns the password used to authenticate the user. * - * This should be the encoded password. On authentication, a plain-text - * password will be salted, encoded, and then compared to this value. + * This should be the hashed password. On authentication, a plain-text + * password will be hashed, and then compared to this value. * - * @return string|null The encoded password if any + * @return string|null The hashed password if any */ public function getPassword(); /** - * Returns the salt that was originally used to encode the password. + * Returns the salt that was originally used to hash the password. * - * This can return null if the password was not encoded using a salt. + * This can return null if the password was not hashed using a salt. * * @return string|null The salt */ diff --git a/Validator/Constraints/UserPasswordValidator.php b/Validator/Constraints/UserPasswordValidator.php index 24b03248..0181ccbc 100644 --- a/Validator/Constraints/UserPasswordValidator.php +++ b/Validator/Constraints/UserPasswordValidator.php @@ -13,7 +13,9 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; +use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; @@ -22,12 +24,19 @@ class UserPasswordValidator extends ConstraintValidator { private $tokenStorage; - private $encoderFactory; + private $hasherFactory; - public function __construct(TokenStorageInterface $tokenStorage, EncoderFactoryInterface $encoderFactory) + /** + * @param PasswordHasherFactoryInterface $hasherFactory + */ + public function __construct(TokenStorageInterface $tokenStorage, $hasherFactory) { + if ($hasherFactory instanceof EncoderFactoryInterface) { + trigger_deprecation('symfony/security-core', '5.3', 'Passing a "%s" instance to the "%s" constructor is deprecated, use "%s" instead.', EncoderFactoryInterface::class, __CLASS__, PasswordHasherFactoryInterface::class); + } + $this->tokenStorage = $tokenStorage; - $this->encoderFactory = $encoderFactory; + $this->hasherFactory = $hasherFactory; } /** @@ -51,9 +60,9 @@ public function validate($password, Constraint $constraint) throw new ConstraintDefinitionException('The User object must implement the UserInterface interface.'); } - $encoder = $this->encoderFactory->getEncoder($user); + $hasher = $this->hasherFactory instanceof EncoderFactoryInterface ? $this->hasherFactory->getEncoder($user) : $this->hasherFactory->getPasswordHasher($user); - if (null === $user->getPassword() || !$encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) { + if (null === $user->getPassword() || !($hasher instanceof PasswordEncoderInterface ? $hasher->isPasswordValid($user->getPassword(), $password, $user->getSalt()) : $hasher->verify($user->getPassword(), $password, $user->getSalt()))) { $this->context->addViolation($constraint->message); } } diff --git a/composer.json b/composer.json index 48a6a46e..424c0775 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "symfony/event-dispatcher-contracts": "^1.1|^2", "symfony/polyfill-php80": "^1.15", "symfony/service-contracts": "^1.1.6|^2", - "symfony/deprecation-contracts": "^2.1" + "symfony/deprecation-contracts": "^2.1", + "symfony/password-hasher": "^5.3" }, "require-dev": { "psr/container": "^1.0", From f8bebde4899a3ba140bba49e5f0f10be60aed700 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Tue, 16 Feb 2021 17:55:35 +0100 Subject: [PATCH 07/35] [Security] Fix some broken BC layers --- Encoder/UserPasswordEncoderInterface.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Encoder/UserPasswordEncoderInterface.php b/Encoder/UserPasswordEncoderInterface.php index 858e8367..99ce4414 100644 --- a/Encoder/UserPasswordEncoderInterface.php +++ b/Encoder/UserPasswordEncoderInterface.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Security\Core\Encoder; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" interface is deprecated, use "%s" on hasher implementations that deal with salts instead.', UserPasswordEncoderInterface::class, UserPasswordHasherInterface::class)); +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" interface is deprecated, use "%s" instead.', UserPasswordEncoderInterface::class, UserPasswordHasherInterface::class)); -use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; +use Symfony\Component\Security\Core\User\UserInterface; /** * UserPasswordEncoderInterface is the interface for the password encoder service. From b9874b16cfaea4b0812b0a539da07684ea2cc039 Mon Sep 17 00:00:00 2001 From: Jan Rosier Date: Thu, 25 Feb 2021 19:01:06 +0100 Subject: [PATCH 08/35] Fix deprecation messages --- Encoder/BasePasswordEncoder.php | 4 ++-- Encoder/EncoderFactory.php | 6 +++--- Encoder/EncoderFactoryInterface.php | 6 +++--- Encoder/MessageDigestPasswordEncoder.php | 4 ++-- Encoder/MigratingPasswordEncoder.php | 4 ++-- Encoder/NativePasswordEncoder.php | 5 ++--- Encoder/PasswordEncoderInterface.php | 6 +++--- Encoder/Pbkdf2PasswordEncoder.php | 5 ++--- Encoder/PlaintextPasswordEncoder.php | 4 ++-- Encoder/SelfSaltingEncoderInterface.php | 4 ++-- Encoder/SodiumPasswordEncoder.php | 4 ++-- Encoder/UserPasswordEncoder.php | 6 +++--- Encoder/UserPasswordEncoderInterface.php | 4 ++-- 13 files changed, 30 insertions(+), 32 deletions(-) diff --git a/Encoder/BasePasswordEncoder.php b/Encoder/BasePasswordEncoder.php index 9c014d9e..613cddd8 100644 --- a/Encoder/BasePasswordEncoder.php +++ b/Encoder/BasePasswordEncoder.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Security\Core\Encoder; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', BasePasswordEncoder::class, CheckPasswordLengthTrait::class)); - use Symfony\Component\PasswordHasher\Hasher\CheckPasswordLengthTrait; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', BasePasswordEncoder::class, CheckPasswordLengthTrait::class)); + /** * BasePasswordEncoder is the base class for all password encoders. * diff --git a/Encoder/EncoderFactory.php b/Encoder/EncoderFactory.php index e90498a3..e2294d5b 100644 --- a/Encoder/EncoderFactory.php +++ b/Encoder/EncoderFactory.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Security\Core\Encoder; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', EncoderFactory::class, PasswordHasherFactory::class)); - -use Symfony\Component\Security\Core\Exception\LogicException; use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface; use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory; +use Symfony\Component\Security\Core\Exception\LogicException; + +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', EncoderFactory::class, PasswordHasherFactory::class)); /** * A generic encoder factory implementation. diff --git a/Encoder/EncoderFactoryInterface.php b/Encoder/EncoderFactoryInterface.php index 65fd12d8..4c2f9fb6 100644 --- a/Encoder/EncoderFactoryInterface.php +++ b/Encoder/EncoderFactoryInterface.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Security\Core\Encoder; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', EncoderFactoryInterface::class, PasswordHasherFactoryInterface::class)); - -use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; +use Symfony\Component\Security\Core\User\UserInterface; + +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', EncoderFactoryInterface::class, PasswordHasherFactoryInterface::class)); /** * EncoderFactoryInterface to support different encoders for different accounts. diff --git a/Encoder/MessageDigestPasswordEncoder.php b/Encoder/MessageDigestPasswordEncoder.php index d4b1fb54..416e940d 100644 --- a/Encoder/MessageDigestPasswordEncoder.php +++ b/Encoder/MessageDigestPasswordEncoder.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Security\Core\Encoder; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', MessageDigestPasswordEncoder::class, MessageDigestPasswordHasher::class)); - use Symfony\Component\PasswordHasher\Hasher\MessageDigestPasswordHasher; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', MessageDigestPasswordEncoder::class, MessageDigestPasswordHasher::class)); + /** * MessageDigestPasswordEncoder uses a message digest algorithm. * diff --git a/Encoder/MigratingPasswordEncoder.php b/Encoder/MigratingPasswordEncoder.php index be178731..f3243258 100644 --- a/Encoder/MigratingPasswordEncoder.php +++ b/Encoder/MigratingPasswordEncoder.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Security\Core\Encoder; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', MigratingPasswordEncoder::class, MigratingPasswordHasher::class)); - use Symfony\Component\PasswordHasher\Hasher\MigratingPasswordHasher; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', MigratingPasswordEncoder::class, MigratingPasswordHasher::class)); + /** * Hashes passwords using the best available encoder. * Validates them using a chain of encoders. diff --git a/Encoder/NativePasswordEncoder.php b/Encoder/NativePasswordEncoder.php index b3bd4b54..f80d1957 100644 --- a/Encoder/NativePasswordEncoder.php +++ b/Encoder/NativePasswordEncoder.php @@ -11,11 +11,10 @@ namespace Symfony\Component\Security\Core\Encoder; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', NativePasswordEncoder::class, NativePasswordHasher::class)); - -use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', NativePasswordEncoder::class, NativePasswordHasher::class)); + /** * Hashes passwords using password_hash(). * diff --git a/Encoder/PasswordEncoderInterface.php b/Encoder/PasswordEncoderInterface.php index ba9216eb..2b55af05 100644 --- a/Encoder/PasswordEncoderInterface.php +++ b/Encoder/PasswordEncoderInterface.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Security\Core\Encoder; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', PasswordEncoderInterface::class, PasswordHasherInterface::class)); - -use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\PasswordHasher\PasswordHasherInterface; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; + +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', PasswordEncoderInterface::class, PasswordHasherInterface::class)); /** * PasswordEncoderInterface is the interface for all encoders. diff --git a/Encoder/Pbkdf2PasswordEncoder.php b/Encoder/Pbkdf2PasswordEncoder.php index a50ad01e..fcc286a0 100644 --- a/Encoder/Pbkdf2PasswordEncoder.php +++ b/Encoder/Pbkdf2PasswordEncoder.php @@ -11,11 +11,10 @@ namespace Symfony\Component\Security\Core\Encoder; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', Pbkdf2PasswordEncoder::class, Pbkdf2PasswordHasher::class)); - -use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', Pbkdf2PasswordEncoder::class, Pbkdf2PasswordHasher::class)); + /** * Pbkdf2PasswordEncoder uses the PBKDF2 (Password-Based Key Derivation Function 2). * diff --git a/Encoder/PlaintextPasswordEncoder.php b/Encoder/PlaintextPasswordEncoder.php index 65fc8502..3165855b 100644 --- a/Encoder/PlaintextPasswordEncoder.php +++ b/Encoder/PlaintextPasswordEncoder.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Security\Core\Encoder; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', PlaintextPasswordEncoder::class, PlaintextPasswordHasher::class)); - use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', PlaintextPasswordEncoder::class, PlaintextPasswordHasher::class)); + /** * PlaintextPasswordEncoder does not do any encoding but is useful in testing environments. * diff --git a/Encoder/SelfSaltingEncoderInterface.php b/Encoder/SelfSaltingEncoderInterface.php index 6bb983dd..d1e93e16 100644 --- a/Encoder/SelfSaltingEncoderInterface.php +++ b/Encoder/SelfSaltingEncoderInterface.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Security\Core\Encoder; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" interface is deprecated, use "%s" on hasher implementations that deal with salts instead.', SelfSaltingEncoderInterface::class, LegacyPasswordHasherInterface::class)); - use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" interface is deprecated, use "%s" on hasher implementations that deal with salts instead.', SelfSaltingEncoderInterface::class, LegacyPasswordHasherInterface::class)); + /** * SelfSaltingEncoderInterface is a marker interface for encoders that do not * require a user-generated salt. diff --git a/Encoder/SodiumPasswordEncoder.php b/Encoder/SodiumPasswordEncoder.php index 480adb4a..95810e59 100644 --- a/Encoder/SodiumPasswordEncoder.php +++ b/Encoder/SodiumPasswordEncoder.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Security\Core\Encoder; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', SodiumPasswordEncoder::class, SodiumPasswordHasher::class)); - use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', SodiumPasswordEncoder::class, SodiumPasswordHasher::class)); + /** * Hashes passwords using libsodium. * diff --git a/Encoder/UserPasswordEncoder.php b/Encoder/UserPasswordEncoder.php index bfe31a4a..32ab07c6 100644 --- a/Encoder/UserPasswordEncoder.php +++ b/Encoder/UserPasswordEncoder.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Security\Core\Encoder; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', UserPasswordEncoder::class, UserPasswordHasher::class)); - -use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher; +use Symfony\Component\Security\Core\User\UserInterface; + +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', UserPasswordEncoder::class, UserPasswordHasher::class)); /** * A generic password encoder. diff --git a/Encoder/UserPasswordEncoderInterface.php b/Encoder/UserPasswordEncoderInterface.php index 99ce4414..a113d108 100644 --- a/Encoder/UserPasswordEncoderInterface.php +++ b/Encoder/UserPasswordEncoderInterface.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Security\Core\Encoder; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" interface is deprecated, use "%s" instead.', UserPasswordEncoderInterface::class, UserPasswordHasherInterface::class)); - use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Security\Core\User\UserInterface; +trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" interface is deprecated, use "%s" instead.', UserPasswordEncoderInterface::class, UserPasswordHasherInterface::class)); + /** * UserPasswordEncoderInterface is the interface for the password encoder service. * From b2b96af1e8070f53bd45652f7b8424449f376095 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Fri, 5 Mar 2021 10:24:52 +0100 Subject: [PATCH 09/35] [Security] Readd accidentally removed property declarations --- Encoder/MigratingPasswordEncoder.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Encoder/MigratingPasswordEncoder.php b/Encoder/MigratingPasswordEncoder.php index f3243258..af881a96 100644 --- a/Encoder/MigratingPasswordEncoder.php +++ b/Encoder/MigratingPasswordEncoder.php @@ -28,6 +28,9 @@ */ final class MigratingPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface { + private $bestEncoder; + private $extraEncoders; + public function __construct(PasswordEncoderInterface $bestEncoder, PasswordEncoderInterface ...$extraEncoders) { $this->bestEncoder = $bestEncoder; From d98d3e0e711688423b78829d1808c97498085666 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Tue, 16 Feb 2021 12:14:29 +0100 Subject: [PATCH 10/35] [Security] Decouple passwords from UserInterface --- .../Provider/DaoAuthenticationProvider.php | 19 +++++++++--- Authentication/Token/AbstractToken.php | 3 ++ Encoder/UserPasswordEncoder.php | 11 +++++++ .../DaoAuthenticationProviderTest.php | 7 +++-- Tests/User/ChainUserProviderTest.php | 10 +++++-- User/ChainUserProvider.php | 10 +++++-- ...gacyPasswordAuthenticatedUserInterface.php | 28 +++++++++++++++++ User/PasswordAuthenticatedUserInterface.php | 30 +++++++++++++++++++ User/PasswordUpgraderInterface.php | 13 ++++---- User/User.php | 2 +- User/UserInterface.php | 4 +++ .../Constraints/UserPasswordValidator.php | 15 ++++++++-- 12 files changed, 130 insertions(+), 22 deletions(-) create mode 100644 User/LegacyPasswordAuthenticatedUserInterface.php create mode 100644 User/PasswordAuthenticatedUserInterface.php diff --git a/Authentication/Provider/DaoAuthenticationProvider.php b/Authentication/Provider/DaoAuthenticationProvider.php index 26beb6b9..eca9357f 100644 --- a/Authentication/Provider/DaoAuthenticationProvider.php +++ b/Authentication/Provider/DaoAuthenticationProvider.php @@ -11,16 +11,18 @@ namespace Symfony\Component\Security\Core\Authentication\Provider; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; -use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; /** * DaoAuthenticationProvider uses a UserProviderInterface to retrieve the user @@ -67,11 +69,20 @@ protected function checkAuthentication(UserInterface $user, UsernamePasswordToke throw new BadCredentialsException('The presented password is invalid.'); } + if (!$user instanceof PasswordAuthenticatedUserInterface) { + trigger_deprecation('symfony/security-core', '5.3', 'Using password-based authentication listeners while not implementing "%s" interface from class "%s" is deprecated.', PasswordAuthenticatedUserInterface::class, get_debug_type($user)); + } + + $salt = $user->getSalt(); + if ($salt && !$user instanceof LegacyPasswordAuthenticatedUserInterface) { + trigger_deprecation('symfony/security-core', '5.3', 'Returning a string from "getSalt()" without implementing the "%s" interface is deprecated, the "%s" class should implement it.', LegacyPasswordAuthenticatedUserInterface::class, get_debug_type($user)); + } + // deprecated since Symfony 5.3 if ($this->hasherFactory instanceof EncoderFactoryInterface) { $encoder = $this->hasherFactory->getEncoder($user); - if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) { + if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $salt)) { throw new BadCredentialsException('The presented password is invalid.'); } @@ -84,12 +95,12 @@ protected function checkAuthentication(UserInterface $user, UsernamePasswordToke $hasher = $this->hasherFactory->getPasswordHasher($user); - if (!$hasher->verify($user->getPassword(), $presentedPassword, $user->getSalt())) { + if (!$hasher->verify($user->getPassword(), $presentedPassword, $salt)) { throw new BadCredentialsException('The presented password is invalid.'); } if ($this->userProvider instanceof PasswordUpgraderInterface && $hasher->needsRehash($user->getPassword())) { - $this->userProvider->upgradePassword($user, $hasher->hash($presentedPassword, $user->getSalt())); + $this->userProvider->upgradePassword($user, $hasher->hash($presentedPassword, $salt)); } } } diff --git a/Authentication/Token/AbstractToken.php b/Authentication/Token/AbstractToken.php index 9106334b..0083ae39 100644 --- a/Authentication/Token/AbstractToken.php +++ b/Authentication/Token/AbstractToken.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Core\Authentication\Token; use Symfony\Component\Security\Core\User\EquatableInterface; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; /** @@ -262,10 +263,12 @@ private function hasUserChanged(UserInterface $user): bool return !(bool) $this->user->isEqualTo($user); } + // @deprecated since Symfony 5.3, check for PasswordAuthenticatedUserInterface on both user objects before comparing passwords if ($this->user->getPassword() !== $user->getPassword()) { return true; } + // @deprecated since Symfony 5.3, check for LegacyPasswordAuthenticatedUserInterface on both user objects before comparing salts if ($this->user->getSalt() !== $user->getSalt()) { return true; } diff --git a/Encoder/UserPasswordEncoder.php b/Encoder/UserPasswordEncoder.php index 32ab07c6..bbbb5d1b 100644 --- a/Encoder/UserPasswordEncoder.php +++ b/Encoder/UserPasswordEncoder.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Security\Core\Encoder; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher; +use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', UserPasswordEncoder::class, UserPasswordHasher::class)); @@ -39,6 +41,15 @@ public function encodePassword(UserInterface $user, string $plainPassword) { $encoder = $this->encoderFactory->getEncoder($user); + if (!$user instanceof PasswordAuthenticatedUserInterface) { + trigger_deprecation('symfony/password-hasher', '5.3', 'Not implementing the "%s" interface while using "%s" is deprecated, the "%s" class should implement it.', PasswordAuthenticatedUserInterface::class, __CLASS__, get_debug_type($user)); + } + + $salt = $user->getSalt(); + if ($salt && !$user instanceof LegacyPasswordAuthenticatedUserInterface) { + trigger_deprecation('symfony/password-hasher', '5.3', 'Returning a string from "getSalt()" without implementing the "%s" interface is deprecated, the "%s" class should implement it.', LegacyPasswordAuthenticatedUserInterface::class, get_debug_type($user)); + } + return $encoder->encodePassword($plainPassword, $user->getSalt()); } diff --git a/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php b/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php index 20e75b80..a308cc6c 100644 --- a/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php @@ -12,6 +12,9 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\Provider; use PHPUnit\Framework\TestCase; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; +use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher; +use Symfony\Component\PasswordHasher\PasswordHasherInterface; use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; @@ -23,9 +26,6 @@ use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; -use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; -use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher; -use Symfony\Component\PasswordHasher\PasswordHasherInterface; class DaoAuthenticationProviderTest extends TestCase { @@ -380,4 +380,5 @@ public function eraseCredentials() } interface PasswordUpgraderProvider extends UserProviderInterface, PasswordUpgraderInterface { + public function upgradePassword(UserInterface $user, string $newHashedPassword): void; } diff --git a/Tests/User/ChainUserProviderTest.php b/Tests/User/ChainUserProviderTest.php index b7e2a411..35075a77 100644 --- a/Tests/User/ChainUserProviderTest.php +++ b/Tests/User/ChainUserProviderTest.php @@ -15,6 +15,7 @@ use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\User\ChainUserProvider; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserInterface; @@ -251,14 +252,14 @@ public function testPasswordUpgrades() { $user = new User('user', 'pwd'); - $provider1 = $this->createMock(PasswordUpgraderInterface::class); + $provider1 = $this->getMockForAbstractClass(MigratingProvider::class); $provider1 ->expects($this->once()) ->method('upgradePassword') ->willThrowException(new UnsupportedUserException('unsupported')) ; - $provider2 = $this->createMock(PasswordUpgraderInterface::class); + $provider2 = $this->getMockForAbstractClass(MigratingProvider::class); $provider2 ->expects($this->once()) ->method('upgradePassword') @@ -269,3 +270,8 @@ public function testPasswordUpgrades() $provider->upgradePassword($user, 'foobar'); } } + +abstract class MigratingProvider implements PasswordUpgraderInterface +{ + abstract public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void; +} diff --git a/User/ChainUserProvider.php b/User/ChainUserProvider.php index 23321250..fedcdb6a 100644 --- a/User/ChainUserProvider.php +++ b/User/ChainUserProvider.php @@ -73,7 +73,7 @@ public function refreshUser(UserInterface $user) foreach ($this->providers as $provider) { try { - if (!$provider->supportsClass(\get_class($user))) { + if (!$provider->supportsClass(get_debug_type($user))) { continue; } @@ -110,10 +110,16 @@ public function supportsClass(string $class) } /** + * @param PasswordAuthenticatedUserInterface $user + * * {@inheritdoc} */ - public function upgradePassword(UserInterface $user, string $newEncodedPassword): void + public function upgradePassword($user, string $newEncodedPassword): void { + if (!$user instanceof PasswordAuthenticatedUserInterface) { + trigger_deprecation('symfony/security-core', '5.3', 'The "%s::upgradePassword()" method expects an instance of "%s" as first argument, the "%s" class should implement it.', PasswordUpgraderInterface::class, PasswordAuthenticatedUserInterface::class, get_debug_type($user)); + } + foreach ($this->providers as $provider) { if ($provider instanceof PasswordUpgraderInterface) { try { diff --git a/User/LegacyPasswordAuthenticatedUserInterface.php b/User/LegacyPasswordAuthenticatedUserInterface.php new file mode 100644 index 00000000..fcffe0b9 --- /dev/null +++ b/User/LegacyPasswordAuthenticatedUserInterface.php @@ -0,0 +1,28 @@ + + * + * 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; + +/** + * For users that can be authenticated using a password/salt couple. + * + * Once all password hashes have been upgraded to a modern algorithm via password migrations, + * implement {@see PasswordAuthenticatedUserInterface} instead. + * + * @author Robin Chalas + */ +interface LegacyPasswordAuthenticatedUserInterface extends PasswordAuthenticatedUserInterface +{ + /** + * Returns the salt that was originally used to hash the password. + */ + public function getSalt(): ?string; +} diff --git a/User/PasswordAuthenticatedUserInterface.php b/User/PasswordAuthenticatedUserInterface.php new file mode 100644 index 00000000..e9d78630 --- /dev/null +++ b/User/PasswordAuthenticatedUserInterface.php @@ -0,0 +1,30 @@ + + * + * 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; + +/** + * For users that can be authenticated using a password. + * + * @author Robin Chalas + * @author Wouter de Jong + */ +interface PasswordAuthenticatedUserInterface +{ + /** + * Returns the hashed password used to authenticate the user. + * + * Usually on authentication, a plain-text password will be compared to this value. + * + * @return string|null The hashed password or null (if not set or erased) + */ + public function getPassword(): ?string; +} diff --git a/User/PasswordUpgraderInterface.php b/User/PasswordUpgraderInterface.php index ef62023d..16195fad 100644 --- a/User/PasswordUpgraderInterface.php +++ b/User/PasswordUpgraderInterface.php @@ -13,15 +13,12 @@ /** * @author Nicolas Grekas + * + * @method void upgradePassword(PasswordAuthenticatedUserInterface|UserInterface $user, string $newHashedPassword) Upgrades the hashed 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. */ interface PasswordUpgraderInterface { - /** - * Upgrades the hashed 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 $newHashedPassword): void; } diff --git a/User/User.php b/User/User.php index 5429baa0..9a749dc2 100644 --- a/User/User.php +++ b/User/User.php @@ -18,7 +18,7 @@ * * @author Fabien Potencier */ -final class User implements UserInterface, EquatableInterface +final class User implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface { private $username; private $password; diff --git a/User/UserInterface.php b/User/UserInterface.php index c005e3ca..47661de0 100644 --- a/User/UserInterface.php +++ b/User/UserInterface.php @@ -52,6 +52,8 @@ public function getRoles(); * This should be the hashed password. On authentication, a plain-text * password will be hashed, and then compared to this value. * + * This method is deprecated since Symfony 5.3, implement it from {@link PasswordAuthenticatedUserInterface} instead. + * * @return string|null The hashed password if any */ public function getPassword(); @@ -61,6 +63,8 @@ public function getPassword(); * * This can return null if the password was not hashed using a salt. * + * This method is deprecated since Symfony 5.3, implement it from {@link LegacyPasswordAuthenticatedUserInterface} instead. + * * @return string|null The salt */ public function getSalt(); diff --git a/Validator/Constraints/UserPasswordValidator.php b/Validator/Constraints/UserPasswordValidator.php index 0181ccbc..bf273f2f 100644 --- a/Validator/Constraints/UserPasswordValidator.php +++ b/Validator/Constraints/UserPasswordValidator.php @@ -11,11 +11,13 @@ namespace Symfony\Component\Security\Core\Validator\Constraints; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; +use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; @@ -32,7 +34,7 @@ class UserPasswordValidator extends ConstraintValidator public function __construct(TokenStorageInterface $tokenStorage, $hasherFactory) { if ($hasherFactory instanceof EncoderFactoryInterface) { - trigger_deprecation('symfony/security-core', '5.3', 'Passing a "%s" instance to the "%s" constructor is deprecated, use "%s" instead.', EncoderFactoryInterface::class, __CLASS__, PasswordHasherFactoryInterface::class); + trigger_deprecation('symfony/security-core', '5.3', 'Passing a "%s" instance to the "%s" constructor is deprecated, use "%s" instead.', EncoderFactoryInterface::class, __CLASS__, PasswordHasherFactoryInterface::class); } $this->tokenStorage = $tokenStorage; @@ -60,6 +62,15 @@ public function validate($password, Constraint $constraint) throw new ConstraintDefinitionException('The User object must implement the UserInterface interface.'); } + if (!$user instanceof PasswordAuthenticatedUserInterface) { + trigger_deprecation('symfony/security-core', '5.3', 'Using the "%s" validation constraint is deprecated.', PasswordAuthenticatedUserInterface::class, get_debug_type($user), UserPassword::class); + } + + $salt = $user->getSalt(); + if ($salt && !$user instanceof LegacyPasswordAuthenticatedUserInterface) { + trigger_deprecation('symfony/security-core', '5.3', 'Returning a string from "getSalt()" without implementing the "%s" interface is deprecated, the "%s" class should implement it.', LegacyPasswordAuthenticatedUserInterface::class, get_debug_type($user)); + } + $hasher = $this->hasherFactory instanceof EncoderFactoryInterface ? $this->hasherFactory->getEncoder($user) : $this->hasherFactory->getPasswordHasher($user); if (null === $user->getPassword() || !($hasher instanceof PasswordEncoderInterface ? $hasher->isPasswordValid($user->getPassword(), $password, $user->getSalt()) : $hasher->verify($user->getPassword(), $password, $user->getSalt()))) { From eefaf4319143c98b43459eb285a8bb63c6d5fffe Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Wed, 10 Mar 2021 10:25:14 +0100 Subject: [PATCH 11/35] Don't use sprintf in trigger_deprecation() calls --- Encoder/BasePasswordEncoder.php | 2 +- Encoder/EncoderFactory.php | 2 +- Encoder/EncoderFactoryInterface.php | 2 +- Encoder/MessageDigestPasswordEncoder.php | 2 +- Encoder/MigratingPasswordEncoder.php | 2 +- Encoder/NativePasswordEncoder.php | 2 +- Encoder/PasswordEncoderInterface.php | 2 +- Encoder/Pbkdf2PasswordEncoder.php | 2 +- Encoder/PlaintextPasswordEncoder.php | 2 +- Encoder/SelfSaltingEncoderInterface.php | 2 +- Encoder/SodiumPasswordEncoder.php | 2 +- Encoder/UserPasswordEncoder.php | 2 +- Encoder/UserPasswordEncoderInterface.php | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Encoder/BasePasswordEncoder.php b/Encoder/BasePasswordEncoder.php index 613cddd8..21c59b3c 100644 --- a/Encoder/BasePasswordEncoder.php +++ b/Encoder/BasePasswordEncoder.php @@ -13,7 +13,7 @@ use Symfony\Component\PasswordHasher\Hasher\CheckPasswordLengthTrait; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', BasePasswordEncoder::class, CheckPasswordLengthTrait::class)); +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', BasePasswordEncoder::class, CheckPasswordLengthTrait::class); /** * BasePasswordEncoder is the base class for all password encoders. diff --git a/Encoder/EncoderFactory.php b/Encoder/EncoderFactory.php index e2294d5b..d1855aa1 100644 --- a/Encoder/EncoderFactory.php +++ b/Encoder/EncoderFactory.php @@ -15,7 +15,7 @@ use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory; use Symfony\Component\Security\Core\Exception\LogicException; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', EncoderFactory::class, PasswordHasherFactory::class)); +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', EncoderFactory::class, PasswordHasherFactory::class); /** * A generic encoder factory implementation. diff --git a/Encoder/EncoderFactoryInterface.php b/Encoder/EncoderFactoryInterface.php index 4c2f9fb6..83dea6c7 100644 --- a/Encoder/EncoderFactoryInterface.php +++ b/Encoder/EncoderFactoryInterface.php @@ -14,7 +14,7 @@ use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; use Symfony\Component\Security\Core\User\UserInterface; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', EncoderFactoryInterface::class, PasswordHasherFactoryInterface::class)); +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', EncoderFactoryInterface::class, PasswordHasherFactoryInterface::class); /** * EncoderFactoryInterface to support different encoders for different accounts. diff --git a/Encoder/MessageDigestPasswordEncoder.php b/Encoder/MessageDigestPasswordEncoder.php index 416e940d..8ea18c05 100644 --- a/Encoder/MessageDigestPasswordEncoder.php +++ b/Encoder/MessageDigestPasswordEncoder.php @@ -13,7 +13,7 @@ use Symfony\Component\PasswordHasher\Hasher\MessageDigestPasswordHasher; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', MessageDigestPasswordEncoder::class, MessageDigestPasswordHasher::class)); +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', MessageDigestPasswordEncoder::class, MessageDigestPasswordHasher::class); /** * MessageDigestPasswordEncoder uses a message digest algorithm. diff --git a/Encoder/MigratingPasswordEncoder.php b/Encoder/MigratingPasswordEncoder.php index af881a96..53d3a58d 100644 --- a/Encoder/MigratingPasswordEncoder.php +++ b/Encoder/MigratingPasswordEncoder.php @@ -13,7 +13,7 @@ use Symfony\Component\PasswordHasher\Hasher\MigratingPasswordHasher; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', MigratingPasswordEncoder::class, MigratingPasswordHasher::class)); +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', MigratingPasswordEncoder::class, MigratingPasswordHasher::class); /** * Hashes passwords using the best available encoder. diff --git a/Encoder/NativePasswordEncoder.php b/Encoder/NativePasswordEncoder.php index f80d1957..bc135bb1 100644 --- a/Encoder/NativePasswordEncoder.php +++ b/Encoder/NativePasswordEncoder.php @@ -13,7 +13,7 @@ use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', NativePasswordEncoder::class, NativePasswordHasher::class)); +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', NativePasswordEncoder::class, NativePasswordHasher::class); /** * Hashes passwords using password_hash(). diff --git a/Encoder/PasswordEncoderInterface.php b/Encoder/PasswordEncoderInterface.php index 2b55af05..45aa24ed 100644 --- a/Encoder/PasswordEncoderInterface.php +++ b/Encoder/PasswordEncoderInterface.php @@ -14,7 +14,7 @@ use Symfony\Component\PasswordHasher\PasswordHasherInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', PasswordEncoderInterface::class, PasswordHasherInterface::class)); +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', PasswordEncoderInterface::class, PasswordHasherInterface::class); /** * PasswordEncoderInterface is the interface for all encoders. diff --git a/Encoder/Pbkdf2PasswordEncoder.php b/Encoder/Pbkdf2PasswordEncoder.php index fcc286a0..d92c12fc 100644 --- a/Encoder/Pbkdf2PasswordEncoder.php +++ b/Encoder/Pbkdf2PasswordEncoder.php @@ -13,7 +13,7 @@ use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', Pbkdf2PasswordEncoder::class, Pbkdf2PasswordHasher::class)); +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', Pbkdf2PasswordEncoder::class, Pbkdf2PasswordHasher::class); /** * Pbkdf2PasswordEncoder uses the PBKDF2 (Password-Based Key Derivation Function 2). diff --git a/Encoder/PlaintextPasswordEncoder.php b/Encoder/PlaintextPasswordEncoder.php index 3165855b..497e9f19 100644 --- a/Encoder/PlaintextPasswordEncoder.php +++ b/Encoder/PlaintextPasswordEncoder.php @@ -13,7 +13,7 @@ use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', PlaintextPasswordEncoder::class, PlaintextPasswordHasher::class)); +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', PlaintextPasswordEncoder::class, PlaintextPasswordHasher::class); /** * PlaintextPasswordEncoder does not do any encoding but is useful in testing environments. diff --git a/Encoder/SelfSaltingEncoderInterface.php b/Encoder/SelfSaltingEncoderInterface.php index d1e93e16..b8740bc9 100644 --- a/Encoder/SelfSaltingEncoderInterface.php +++ b/Encoder/SelfSaltingEncoderInterface.php @@ -13,7 +13,7 @@ use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" interface is deprecated, use "%s" on hasher implementations that deal with salts instead.', SelfSaltingEncoderInterface::class, LegacyPasswordHasherInterface::class)); +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" interface is deprecated, use "%s" on hasher implementations that deal with salts instead.', SelfSaltingEncoderInterface::class, LegacyPasswordHasherInterface::class); /** * SelfSaltingEncoderInterface is a marker interface for encoders that do not diff --git a/Encoder/SodiumPasswordEncoder.php b/Encoder/SodiumPasswordEncoder.php index 95810e59..d2d71f48 100644 --- a/Encoder/SodiumPasswordEncoder.php +++ b/Encoder/SodiumPasswordEncoder.php @@ -13,7 +13,7 @@ use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', SodiumPasswordEncoder::class, SodiumPasswordHasher::class)); +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', SodiumPasswordEncoder::class, SodiumPasswordHasher::class); /** * Hashes passwords using libsodium. diff --git a/Encoder/UserPasswordEncoder.php b/Encoder/UserPasswordEncoder.php index bbbb5d1b..7b29918c 100644 --- a/Encoder/UserPasswordEncoder.php +++ b/Encoder/UserPasswordEncoder.php @@ -16,7 +16,7 @@ use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" class is deprecated, use "%s" instead.', UserPasswordEncoder::class, UserPasswordHasher::class)); +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', UserPasswordEncoder::class, UserPasswordHasher::class); /** * A generic password encoder. diff --git a/Encoder/UserPasswordEncoderInterface.php b/Encoder/UserPasswordEncoderInterface.php index a113d108..488777c1 100644 --- a/Encoder/UserPasswordEncoderInterface.php +++ b/Encoder/UserPasswordEncoderInterface.php @@ -14,7 +14,7 @@ use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Security\Core\User\UserInterface; -trigger_deprecation('symfony/security-core', '5.3', sprintf('The "%s" interface is deprecated, use "%s" instead.', UserPasswordEncoderInterface::class, UserPasswordHasherInterface::class)); +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" interface is deprecated, use "%s" instead.', UserPasswordEncoderInterface::class, UserPasswordHasherInterface::class); /** * UserPasswordEncoderInterface is the interface for the password encoder service. From 8fc53b196c08a02ae076df78fddc115e9c12325d Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Sun, 7 Mar 2021 14:54:55 +0100 Subject: [PATCH 12/35] [Security] Rename User to InMemoryUser --- .../DaoAuthenticationProviderTest.php | 10 +- .../LdapBindAuthenticationProviderTest.php | 14 +- .../RememberMeAuthenticationProviderTest.php | 4 +- .../Token/switch-user-token-4.4.txt | Bin 0 -> 1165 bytes .../Authorization/ExpressionLanguageTest.php | 4 +- Tests/SecurityTest.php | 4 +- Tests/User/ChainUserProviderTest.php | 4 +- Tests/User/InMemoryUserCheckerTest.php | 41 ++++++ Tests/User/InMemoryUserProviderTest.php | 19 +++ Tests/User/InMemoryUserTest.php | 105 ++++++++++++++ Tests/User/UserCheckerTest.php | 3 + Tests/User/UserTest.php | 3 + User/InMemoryUser.php | 133 ++++++++++++++++++ User/InMemoryUserChecker.php | 70 +++++++++ User/InMemoryUserProvider.php | 32 ++++- User/User.php | 8 +- User/UserChecker.php | 53 ++----- 17 files changed, 435 insertions(+), 72 deletions(-) create mode 100644 Tests/Authentication/Token/switch-user-token-4.4.txt create mode 100644 Tests/User/InMemoryUserCheckerTest.php create mode 100644 Tests/User/InMemoryUserTest.php create mode 100644 User/InMemoryUser.php create mode 100644 User/InMemoryUserChecker.php diff --git a/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php b/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php index a308cc6c..46b5624b 100644 --- a/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php @@ -21,8 +21,8 @@ use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; -use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -174,7 +174,7 @@ public function testCheckAuthenticationWhenCredentialsAre0() $method->invoke( $provider, - new User('username', 'password'), + new InMemoryUser('username', 'password'), $token ); } @@ -198,7 +198,7 @@ public function testCheckAuthenticationWhenCredentialsAreNotValid() ->willReturn('foo') ; - $method->invoke($provider, new User('username', 'password'), $token); + $method->invoke($provider, new InMemoryUser('username', 'password'), $token); } public function testCheckAuthenticationDoesNotReauthenticateWhenPasswordHasChanged() @@ -270,12 +270,12 @@ public function testCheckAuthentication() ->willReturn('foo') ; - $method->invoke($provider, new User('username', 'password'), $token); + $method->invoke($provider, new InMemoryUser('username', 'password'), $token); } public function testPasswordUpgrades() { - $user = new User('user', 'pwd'); + $user = new InMemoryUser('user', 'pwd'); $hasher = $this->createMock(PasswordHasherInterface::class); $hasher->expects($this->once()) diff --git a/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php b/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php index 0605df44..c4750844 100644 --- a/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php @@ -20,7 +20,7 @@ use Symfony\Component\Security\Core\Authentication\Provider\LdapBindAuthenticationProvider; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -41,7 +41,7 @@ public function testEmptyPasswordShouldThrowAnException() $reflection = new \ReflectionMethod($provider, 'checkAuthentication'); $reflection->setAccessible(true); - $reflection->invoke($provider, new User('foo', null), new UsernamePasswordToken('foo', '', 'key')); + $reflection->invoke($provider, new InMemoryUser('foo', null), new UsernamePasswordToken('foo', '', 'key')); } public function testNullPasswordShouldThrowAnException() @@ -56,7 +56,7 @@ public function testNullPasswordShouldThrowAnException() $reflection = new \ReflectionMethod($provider, 'checkAuthentication'); $reflection->setAccessible(true); - $reflection->invoke($provider, new User('foo', null), new UsernamePasswordToken('foo', null, 'key')); + $reflection->invoke($provider, new InMemoryUser('foo', null), new UsernamePasswordToken('foo', null, 'key')); } public function testBindFailureShouldThrowAnException() @@ -76,7 +76,7 @@ public function testBindFailureShouldThrowAnException() $reflection = new \ReflectionMethod($provider, 'checkAuthentication'); $reflection->setAccessible(true); - $reflection->invoke($provider, new User('foo', null), new UsernamePasswordToken('foo', 'bar', 'key')); + $reflection->invoke($provider, new InMemoryUser('foo', null), new UsernamePasswordToken('foo', 'bar', 'key')); } public function testRetrieveUser() @@ -136,7 +136,7 @@ public function testQueryForDn() $reflection = new \ReflectionMethod($provider, 'checkAuthentication'); $reflection->setAccessible(true); - $reflection->invoke($provider, new User('foo', null), new UsernamePasswordToken('foo', 'bar', 'key')); + $reflection->invoke($provider, new InMemoryUser('foo', null), new UsernamePasswordToken('foo', 'bar', 'key')); } public function testQueryWithUserForDn() @@ -178,7 +178,7 @@ public function testQueryWithUserForDn() $reflection = new \ReflectionMethod($provider, 'checkAuthentication'); $reflection->setAccessible(true); - $reflection->invoke($provider, new User('foo', null), new UsernamePasswordToken('foo', 'bar', 'key')); + $reflection->invoke($provider, new InMemoryUser('foo', null), new UsernamePasswordToken('foo', 'bar', 'key')); } public function testEmptyQueryResultShouldThrowAnException() @@ -214,6 +214,6 @@ public function testEmptyQueryResultShouldThrowAnException() $reflection = new \ReflectionMethod($provider, 'checkAuthentication'); $reflection->setAccessible(true); - $reflection->invoke($provider, new User('foo', null), new UsernamePasswordToken('foo', 'bar', 'key')); + $reflection->invoke($provider, new InMemoryUser('foo', null), new UsernamePasswordToken('foo', 'bar', 'key')); } } diff --git a/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php b/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php index d5bd2d40..41994e7b 100644 --- a/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php @@ -19,7 +19,7 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\DisabledException; use Symfony\Component\Security\Core\Exception\LogicException; -use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -59,7 +59,7 @@ public function testAuthenticateThrowsOnNonUserInterfaceInstance() $this->expectExceptionMessage('Method "Symfony\Component\Security\Core\Authentication\Token\RememberMeToken::getUser()" must return a "Symfony\Component\Security\Core\User\UserInterface" instance, "string" returned.'); $provider = $this->getProvider(); - $token = new RememberMeToken(new User('dummyuser', null), 'foo', 'test'); + $token = new RememberMeToken(new InMemoryUser('dummyuser', null), 'foo', 'test'); $token->setUser('stringish-user'); $provider->authenticate($token); } diff --git a/Tests/Authentication/Token/switch-user-token-4.4.txt b/Tests/Authentication/Token/switch-user-token-4.4.txt new file mode 100644 index 0000000000000000000000000000000000000000..f359ec4a3ddde5cb7565e4280920ea94eab05754 GIT binary patch literal 1165 zcmeHF&1%Ci4DPe|36eHxGx;<^4}+~8&3ZEoCv}Op#x8b32_f%3Da~OpXd$=4F2)l6 zeymR^EE8Z^TOF-wMQW?FHOkZ?Q$^!+O)aOyb5obt)rG9JHR8j5Ds5MH8us)W}M`OYbk%9Y%pDS^eUd5JKlsjUBCJe7NP(G2Uwku|)Ao zYQwmOIhPP$U2P$Hy6=h%h!^vwD(hM*7}B9wjM&+|Y5f7un(;s65^a4+qv$%3?L1C} z@ePq+eiJMyBlD9wFrE*?ikFjEoINSeaJm=;W$pn7wA;R}Klj;shfxe!kOYOW!E=F+ f1L&|H-GW_#1fcB3je6k3ZHbHcpZJYM>HGc%Z1|PJ literal 0 HcmV?d00001 diff --git a/Tests/Authorization/ExpressionLanguageTest.php b/Tests/Authorization/ExpressionLanguageTest.php index 1276da80..09559788 100644 --- a/Tests/Authorization/ExpressionLanguageTest.php +++ b/Tests/Authorization/ExpressionLanguageTest.php @@ -23,7 +23,7 @@ use Symfony\Component\Security\Core\Authorization\ExpressionLanguage; use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter; -use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Security\Core\User\InMemoryUser; class ExpressionLanguageTest extends TestCase { @@ -49,7 +49,7 @@ public function testIsAuthenticated($token, $expression, $result) public function provider() { $roles = ['ROLE_USER', 'ROLE_ADMIN']; - $user = new User('username', 'password', $roles); + $user = new InMemoryUser('username', 'password', $roles); $noToken = null; $anonymousToken = new AnonymousToken('firewall', 'anon.'); diff --git a/Tests/SecurityTest.php b/Tests/SecurityTest.php index 93527599..489b1bea 100644 --- a/Tests/SecurityTest.php +++ b/Tests/SecurityTest.php @@ -18,7 +18,7 @@ use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Security; -use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Security\Core\User\InMemoryUser; class SecurityTest extends TestCase { @@ -66,7 +66,7 @@ public function getUserTests() yield [new StringishUser(), null]; - $user = new User('nice_user', 'foo'); + $user = new InMemoryUser('nice_user', 'foo'); yield [$user, $user]; } diff --git a/Tests/User/ChainUserProviderTest.php b/Tests/User/ChainUserProviderTest.php index 35075a77..74d0cc13 100644 --- a/Tests/User/ChainUserProviderTest.php +++ b/Tests/User/ChainUserProviderTest.php @@ -15,9 +15,9 @@ use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\User\ChainUserProvider; +use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; -use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -250,7 +250,7 @@ public function testAcceptsTraversable() public function testPasswordUpgrades() { - $user = new User('user', 'pwd'); + $user = new InMemoryUser('user', 'pwd'); $provider1 = $this->getMockForAbstractClass(MigratingProvider::class); $provider1 diff --git a/Tests/User/InMemoryUserCheckerTest.php b/Tests/User/InMemoryUserCheckerTest.php new file mode 100644 index 00000000..8b01e5f0 --- /dev/null +++ b/Tests/User/InMemoryUserCheckerTest.php @@ -0,0 +1,41 @@ + + * + * 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\User; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Exception\DisabledException; +use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\Component\Security\Core\User\InMemoryUserChecker; +use Symfony\Component\Security\Core\User\UserInterface; + +class InMemoryUserCheckerTest extends TestCase +{ + public function testCheckPostAuthNotAdvancedUserInterface() + { + $checker = new InMemoryUserChecker(); + + $this->assertNull($checker->checkPostAuth($this->createMock(UserInterface::class))); + } + + public function testCheckPostAuthPass() + { + $checker = new InMemoryUserChecker(); + $this->assertNull($checker->checkPostAuth(new InMemoryUser('John', 'password'))); + } + + public function testCheckPreAuthDisabled() + { + $this->expectException(DisabledException::class); + $checker = new InMemoryUserChecker(); + $checker->checkPreAuth(new InMemoryUser('John', 'password', [], false)); + } +} diff --git a/Tests/User/InMemoryUserProviderTest.php b/Tests/User/InMemoryUserProviderTest.php index 4f1438ad..d3b3eccf 100644 --- a/Tests/User/InMemoryUserProviderTest.php +++ b/Tests/User/InMemoryUserProviderTest.php @@ -12,12 +12,16 @@ namespace Symfony\Component\Security\Core\Tests\User; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Core\User\User; class InMemoryUserProviderTest extends TestCase { + use ExpectDeprecationTrait; + public function testConstructor() { $provider = $this->createProvider(); @@ -29,6 +33,21 @@ public function testConstructor() } public function testRefresh() + { + $user = new InMemoryUser('fabien', 'bar'); + + $provider = $this->createProvider(); + + $refreshedUser = $provider->refreshUser($user); + $this->assertEquals('foo', $refreshedUser->getPassword()); + $this->assertEquals(['ROLE_USER'], $refreshedUser->getRoles()); + $this->assertFalse($refreshedUser->isEnabled()); + } + + /** + * @group legacy + */ + public function testRefreshWithLegacyUser() { $user = new User('fabien', 'bar'); diff --git a/Tests/User/InMemoryUserTest.php b/Tests/User/InMemoryUserTest.php new file mode 100644 index 00000000..885d1f73 --- /dev/null +++ b/Tests/User/InMemoryUserTest.php @@ -0,0 +1,105 @@ + + * + * 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\User; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\User\EquatableInterface; +use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\Component\Security\Core\User\UserInterface; + +class InMemoryUserTest extends TestCase +{ + public function testConstructorException() + { + $this->expectException(\InvalidArgumentException::class); + new InMemoryUser('', 'superpass'); + } + + public function testGetRoles() + { + $user = new InMemoryUser('fabien', 'superpass'); + $this->assertEquals([], $user->getRoles()); + + $user = new InMemoryUser('fabien', 'superpass', ['ROLE_ADMIN']); + $this->assertEquals(['ROLE_ADMIN'], $user->getRoles()); + } + + public function testGetPassword() + { + $user = new InMemoryUser('fabien', 'superpass'); + $this->assertEquals('superpass', $user->getPassword()); + } + + public function testGetUsername() + { + $user = new InMemoryUser('fabien', 'superpass'); + $this->assertEquals('fabien', $user->getUsername()); + } + + public function testGetSalt() + { + $user = new InMemoryUser('fabien', 'superpass'); + $this->assertNull($user->getSalt()); + } + + public function testIsEnabled() + { + $user = new InMemoryUser('mathilde', 'k'); + $this->assertTrue($user->isEnabled()); + + $user = new InMemoryUser('robin', 'superpass', [], false); + $this->assertFalse($user->isEnabled()); + } + + public function testEraseCredentials() + { + $user = new InMemoryUser('fabien', 'superpass'); + $user->eraseCredentials(); + $this->assertEquals('superpass', $user->getPassword()); + } + + public function testToString() + { + $user = new InMemoryUser('fabien', 'superpass'); + $this->assertEquals('fabien', (string) $user); + } + + /** + * @dataProvider isEqualToData + * + * @param bool $expectation + * @param EquatableInterface|UserInterface $a + * @param EquatableInterface|UserInterface $b + */ + public function testIsEqualTo($expectation, $a, $b) + { + $this->assertSame($expectation, $a->isEqualTo($b)); + $this->assertSame($expectation, $b->isEqualTo($a)); + } + + public static function isEqualToData() + { + return [ + [true, new InMemoryUser('username', 'password'), new InMemoryUser('username', 'password')], + [false, new InMemoryUser('username', 'password', ['ROLE']), new InMemoryUser('username', 'password')], + [false, new InMemoryUser('username', 'password', ['ROLE']), new InMemoryUser('username', 'password', ['NO ROLE'])], + [false, new InMemoryUser('diff', 'diff'), new InMemoryUser('username', 'password')], + [false, new InMemoryUser('diff', 'diff', [], false), new InMemoryUser('username', 'password')], + ]; + } + + public function testIsEqualToWithDifferentUser() + { + $user = new InMemoryUser('username', 'password'); + $this->assertFalse($user->isEqualTo($this->createMock(UserInterface::class))); + } +} diff --git a/Tests/User/UserCheckerTest.php b/Tests/User/UserCheckerTest.php index b6d1e682..728d935b 100644 --- a/Tests/User/UserCheckerTest.php +++ b/Tests/User/UserCheckerTest.php @@ -20,6 +20,9 @@ use Symfony\Component\Security\Core\User\UserChecker; use Symfony\Component\Security\Core\User\UserInterface; +/** + * @group legacy + */ class UserCheckerTest extends TestCase { public function testCheckPostAuthNotAdvancedUserInterface() diff --git a/Tests/User/UserTest.php b/Tests/User/UserTest.php index 21e0ac77..143479de 100644 --- a/Tests/User/UserTest.php +++ b/Tests/User/UserTest.php @@ -16,6 +16,9 @@ use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserInterface; +/** + * @group legacy + */ class UserTest extends TestCase { public function testConstructorException() diff --git a/User/InMemoryUser.php b/User/InMemoryUser.php new file mode 100644 index 00000000..fafefe3a --- /dev/null +++ b/User/InMemoryUser.php @@ -0,0 +1,133 @@ + + * + * 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; + +/** + * UserInterface implementation used by the in-memory user provider. + * + * This should not be used for anything else. + * + * @author Robin Chalas + * @author Fabien Potencier + */ +final class InMemoryUser implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface +{ + private $username; + private $password; + private $enabled; + private $roles; + + /** + * @param string[] $roles + */ + public function __construct(string $username, ?string $password, array $roles = [], bool $enabled = true) + { + if ('' === $username) { + throw new \InvalidArgumentException('The username cannot be empty.'); + } + + $this->username = $username; + $this->password = $password; + $this->roles = $roles; + $this->enabled = $enabled; + } + + public function __toString(): string + { + return $this->getUsername(); + } + + /** + * {@inheritdoc} + */ + public function getRoles(): array + { + return $this->roles; + } + + /** + * {@inheritdoc} + */ + public function getPassword(): ?string + { + return $this->password; + } + + /** + * {@inheritdoc} + */ + public function getSalt(): ?string + { + return null; + } + + /** + * {@inheritdoc} + */ + public function getUsername(): string + { + return $this->username; + } + + /** + * Checks whether the user is enabled. + * + * Internally, if this method returns false, the authentication system + * will throw a DisabledException and prevent login. + * + * @return bool true if the user is enabled, false otherwise + * + * @see DisabledException + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * {@inheritdoc} + */ + public function eraseCredentials() + { + } + + /** + * {@inheritdoc} + */ + public function isEqualTo(UserInterface $user): bool + { + if (!$user instanceof self) { + return false; + } + + if ($this->getPassword() !== $user->getPassword()) { + return false; + } + + $currentRoles = array_map('strval', (array) $this->getRoles()); + $newRoles = array_map('strval', (array) $user->getRoles()); + $rolesChanged = \count($currentRoles) !== \count($newRoles) || \count($currentRoles) !== \count(array_intersect($currentRoles, $newRoles)); + if ($rolesChanged) { + return false; + } + + if ($this->getUsername() !== $user->getUsername()) { + return false; + } + + if ($this->isEnabled() !== $user->isEnabled()) { + return false; + } + + return true; + } +} diff --git a/User/InMemoryUserChecker.php b/User/InMemoryUserChecker.php new file mode 100644 index 00000000..a23abc2f --- /dev/null +++ b/User/InMemoryUserChecker.php @@ -0,0 +1,70 @@ + + * + * 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; + +use Symfony\Component\Security\Core\Exception\AccountExpiredException; +use Symfony\Component\Security\Core\Exception\CredentialsExpiredException; +use Symfony\Component\Security\Core\Exception\DisabledException; +use Symfony\Component\Security\Core\Exception\LockedException; + +/** + * Checks the state of the in-memory user account. + * + * @author Fabien Potencier + */ +class InMemoryUserChecker implements UserCheckerInterface +{ + public function checkPreAuth(UserInterface $user) + { + // @deprecated since Symfony 5.3, in 6.0 change to: + // if (!$user instanceof InMemoryUser) { + if (!$user instanceof InMemoryUser && !$user instanceof User) { + return; + } + + if (!$user->isEnabled()) { + $ex = new DisabledException('User account is disabled.'); + $ex->setUser($user); + throw $ex; + } + + // @deprecated since Symfony 5.3 + if ($user instanceof User) { + if (!$user->isAccountNonLocked()) { + $ex = new LockedException('User account is locked.'); + $ex->setUser($user); + throw $ex; + } + + if (!$user->isAccountNonExpired()) { + $ex = new AccountExpiredException('User account has expired.'); + $ex->setUser($user); + throw $ex; + } + } + } + + public function checkPostAuth(UserInterface $user) + { + // @deprecated since Symfony 5.3, noop in 6.0 + if (!$user instanceof User) { + return; + } + + if (!$user->isCredentialsNonExpired()) { + $ex = new CredentialsExpiredException('User credentials have expired.'); + $ex->setUser($user); + throw $ex; + } + } +} +class_alias(InMemoryUserChecker::class, UserChecker::class); diff --git a/User/InMemoryUserProvider.php b/User/InMemoryUserProvider.php index 78482d5c..5445d559 100644 --- a/User/InMemoryUserProvider.php +++ b/User/InMemoryUserProvider.php @@ -38,7 +38,7 @@ public function __construct(array $users = []) $password = $attributes['password'] ?? null; $enabled = $attributes['enabled'] ?? true; $roles = $attributes['roles'] ?? []; - $user = new User($username, $password, $roles, $enabled, true, true, true); + $user = new InMemoryUser($username, $password, $roles, $enabled); $this->createUser($user); } @@ -65,7 +65,7 @@ public function loadUserByUsername(string $username) { $user = $this->getUser($username); - return new User($user->getUsername(), $user->getPassword(), $user->getRoles(), $user->isEnabled(), $user->isAccountNonExpired(), $user->isCredentialsNonExpired(), $user->isAccountNonLocked()); + return new InMemoryUser($user->getUsername(), $user->getPassword(), $user->getRoles(), $user->isEnabled()); } /** @@ -73,13 +73,28 @@ public function loadUserByUsername(string $username) */ public function refreshUser(UserInterface $user) { - if (!$user instanceof User) { + if (!$user instanceof InMemoryUser && !$user instanceof User) { throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_debug_type($user))); } $storedUser = $this->getUser($user->getUsername()); - return new User($storedUser->getUsername(), $storedUser->getPassword(), $storedUser->getRoles(), $storedUser->isEnabled(), $storedUser->isAccountNonExpired(), $storedUser->isCredentialsNonExpired() && $storedUser->getPassword() === $user->getPassword(), $storedUser->isAccountNonLocked()); + // @deprecated since Symfony 5.3 + if ($user instanceof User) { + if (!$storedUser instanceof User) { + $accountNonExpired = true; + $credentialsNonExpired = $storedUser->getPassword() === $user->getPassword(); + $accountNonLocked = true; + } else { + $accountNonExpired = $storedUser->isAccountNonExpired(); + $credentialsNonExpired = $storedUser->isCredentialsNonExpired() && $storedUser->getPassword() === $user->getPassword(); + $accountNonLocked = $storedUser->isAccountNonLocked(); + } + + return new User($storedUser->getUsername(), $storedUser->getPassword(), $storedUser->getRoles(), $storedUser->isEnabled(), $accountNonExpired, $credentialsNonExpired, $accountNonLocked); + } + + return new InMemoryUser($storedUser->getUsername(), $storedUser->getPassword(), $storedUser->getRoles(), $storedUser->isEnabled()); } /** @@ -87,7 +102,12 @@ public function refreshUser(UserInterface $user) */ public function supportsClass(string $class) { - return 'Symfony\Component\Security\Core\User\User' === $class; + // @deprecated since Symfony 5.3 + if (User::class === $class) { + return true; + } + + return InMemoryUser::class == $class; } /** @@ -95,7 +115,7 @@ public function supportsClass(string $class) * * @throws UsernameNotFoundException if user whose given username does not exist */ - private function getUser(string $username): User + private function getUser(string $username)/*: InMemoryUser */ { if (!isset($this->users[strtolower($username)])) { $ex = new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username)); diff --git a/User/User.php b/User/User.php index 39219431..02ed02d0 100644 --- a/User/User.php +++ b/User/User.php @@ -11,12 +11,16 @@ namespace Symfony\Component\Security\Core\User; +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', User::class, InMemoryUser::class); + /** * User is the user implementation used by the in-memory user provider. * * This should not be used for anything else. * * @author Fabien Potencier + * + * @deprecated since Symfony 5.3, use {@link InMemoryUser} instead */ final class User implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface { @@ -171,8 +175,8 @@ public function isEqualTo(UserInterface $user): bool return false; } - $currentRoles = array_map('strval', (array) $this->getRoles()); - $newRoles = array_map('strval', (array) $user->getRoles()); + $currentRoles = array_map('strval', (array)$this->getRoles()); + $newRoles = array_map('strval', (array)$user->getRoles()); $rolesChanged = \count($currentRoles) !== \count($newRoles) || \count($currentRoles) !== \count(array_intersect($currentRoles, $newRoles)); if ($rolesChanged) { return false; diff --git a/User/UserChecker.php b/User/UserChecker.php index 810ab21c..0c2948a6 100644 --- a/User/UserChecker.php +++ b/User/UserChecker.php @@ -16,54 +16,19 @@ use Symfony\Component\Security\Core\Exception\DisabledException; use Symfony\Component\Security\Core\Exception\LockedException; -/** - * UserChecker checks the user account flags. - * - * @author Fabien Potencier - */ -class UserChecker implements UserCheckerInterface -{ - /** - * {@inheritdoc} - */ - public function checkPreAuth(UserInterface $user) - { - if (!$user instanceof User) { - return; - } +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', UserChecker::class, InMemoryUserChecker::class); - if (!$user->isAccountNonLocked()) { - $ex = new LockedException('User account is locked.'); - $ex->setUser($user); - throw $ex; - } - - if (!$user->isEnabled()) { - $ex = new DisabledException('User account is disabled.'); - $ex->setUser($user); - throw $ex; - } - - if (!$user->isAccountNonExpired()) { - $ex = new AccountExpiredException('User account has expired.'); - $ex->setUser($user); - throw $ex; - } - } +class_exists(InMemoryUserChecker::class); +if (false) { /** - * {@inheritdoc} + * UserChecker checks the user account flags. + * + * @author Fabien Potencier + * + * @deprecated since Symfony 5.3, use {@link InMemoryUserChecker} instead */ - public function checkPostAuth(UserInterface $user) + class UserChecker { - if (!$user instanceof User) { - return; - } - - if (!$user->isCredentialsNonExpired()) { - $ex = new CredentialsExpiredException('User credentials have expired.'); - $ex->setUser($user); - throw $ex; - } } } From ab98b0a5c396b99a3414c07605c143e987121ace Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Tue, 16 Mar 2021 21:07:52 +0100 Subject: [PATCH 13/35] [Security] Fix BC layer --- Tests/User/InMemoryUserProviderTest.php | 6 +- User/InMemoryUser.php | 105 ++++++------------------ User/InMemoryUserChecker.php | 4 +- User/InMemoryUserProvider.php | 4 +- User/User.php | 30 ++++--- 5 files changed, 48 insertions(+), 101 deletions(-) diff --git a/Tests/User/InMemoryUserProviderTest.php b/Tests/User/InMemoryUserProviderTest.php index d3b3eccf..f9d27c8f 100644 --- a/Tests/User/InMemoryUserProviderTest.php +++ b/Tests/User/InMemoryUserProviderTest.php @@ -74,7 +74,7 @@ protected function createProvider(): InMemoryUserProvider public function testCreateUser() { $provider = new InMemoryUserProvider(); - $provider->createUser(new User('fabien', 'foo')); + $provider->createUser(new InMemoryUser('fabien', 'foo')); $user = $provider->loadUserByUsername('fabien'); $this->assertEquals('foo', $user->getPassword()); @@ -84,8 +84,8 @@ public function testCreateUserAlreadyExist() { $this->expectException(\LogicException::class); $provider = new InMemoryUserProvider(); - $provider->createUser(new User('fabien', 'foo')); - $provider->createUser(new User('fabien', 'foo')); + $provider->createUser(new InMemoryUser('fabien', 'foo')); + $provider->createUser(new InMemoryUser('fabien', 'foo')); } public function testLoadUserByUsernameDoesNotExist() diff --git a/User/InMemoryUser.php b/User/InMemoryUser.php index fafefe3a..39da71e3 100644 --- a/User/InMemoryUser.php +++ b/User/InMemoryUser.php @@ -19,115 +19,58 @@ * @author Robin Chalas * @author Fabien Potencier */ -final class InMemoryUser implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface +final class InMemoryUser extends User { - private $username; - private $password; - private $enabled; - private $roles; - /** - * @param string[] $roles + * {@inheritdoc} + * + * @deprecated since Symfony 5.3 */ - public function __construct(string $username, ?string $password, array $roles = [], bool $enabled = true) + public function isAccountNonExpired(): bool { - if ('' === $username) { - throw new \InvalidArgumentException('The username cannot be empty.'); - } + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, you should stop using it.', __METHOD__); - $this->username = $username; - $this->password = $password; - $this->roles = $roles; - $this->enabled = $enabled; - } - - public function __toString(): string - { - return $this->getUsername(); + return parent::isAccountNonExpired(); } /** * {@inheritdoc} + * + * @deprecated since Symfony 5.3 */ - public function getRoles(): array + public function isAccountNonLocked(): bool { - return $this->roles; - } + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, you should stop using it.', __METHOD__); - /** - * {@inheritdoc} - */ - public function getPassword(): ?string - { - return $this->password; + return parent::isAccountNonLocked(); } /** * {@inheritdoc} + * + * @deprecated since Symfony 5.3 */ - public function getSalt(): ?string + public function isCredentialsNonExpired(): bool { - return null; - } + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, you should stop using it.', __METHOD__); - /** - * {@inheritdoc} - */ - public function getUsername(): string - { - return $this->username; + return parent::isCredentialsNonExpired(); } /** - * Checks whether the user is enabled. - * - * Internally, if this method returns false, the authentication system - * will throw a DisabledException and prevent login. - * - * @return bool true if the user is enabled, false otherwise - * - * @see DisabledException + * @deprecated since Symfony 5.3 */ - public function isEnabled(): bool + public function getExtraFields(): array { - return $this->enabled; - } + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, you should stop using it.', __METHOD__); - /** - * {@inheritdoc} - */ - public function eraseCredentials() - { + return parent::getExtraFields(); } - /** - * {@inheritdoc} - */ - public function isEqualTo(UserInterface $user): bool + public function setPassword(string $password) { - if (!$user instanceof self) { - return false; - } - - if ($this->getPassword() !== $user->getPassword()) { - return false; - } - - $currentRoles = array_map('strval', (array) $this->getRoles()); - $newRoles = array_map('strval', (array) $user->getRoles()); - $rolesChanged = \count($currentRoles) !== \count($newRoles) || \count($currentRoles) !== \count(array_intersect($currentRoles, $newRoles)); - if ($rolesChanged) { - return false; - } - - if ($this->getUsername() !== $user->getUsername()) { - return false; - } - - if ($this->isEnabled() !== $user->isEnabled()) { - return false; - } + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, you should stop using it.', __METHOD__); - return true; + parent::setPassword($password); } } diff --git a/User/InMemoryUserChecker.php b/User/InMemoryUserChecker.php index a23abc2f..6f661c76 100644 --- a/User/InMemoryUserChecker.php +++ b/User/InMemoryUserChecker.php @@ -38,7 +38,7 @@ public function checkPreAuth(UserInterface $user) } // @deprecated since Symfony 5.3 - if ($user instanceof User) { + if (User::class === \get_class($user)) { if (!$user->isAccountNonLocked()) { $ex = new LockedException('User account is locked.'); $ex->setUser($user); @@ -56,7 +56,7 @@ public function checkPreAuth(UserInterface $user) public function checkPostAuth(UserInterface $user) { // @deprecated since Symfony 5.3, noop in 6.0 - if (!$user instanceof User) { + if (User::class !== \get_class($user)) { return; } diff --git a/User/InMemoryUserProvider.php b/User/InMemoryUserProvider.php index 5445d559..c79f96e2 100644 --- a/User/InMemoryUserProvider.php +++ b/User/InMemoryUserProvider.php @@ -80,8 +80,8 @@ public function refreshUser(UserInterface $user) $storedUser = $this->getUser($user->getUsername()); // @deprecated since Symfony 5.3 - if ($user instanceof User) { - if (!$storedUser instanceof User) { + if (User::class === \get_class($user)) { + if (User::class !== \get_class($storedUser)) { $accountNonExpired = true; $credentialsNonExpired = $storedUser->getPassword() === $user->getPassword(); $accountNonLocked = true; diff --git a/User/User.php b/User/User.php index 02ed02d0..045f03bf 100644 --- a/User/User.php +++ b/User/User.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Security\Core\User; -trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', User::class, InMemoryUser::class); - /** * User is the user implementation used by the in-memory user provider. * @@ -22,7 +20,7 @@ * * @deprecated since Symfony 5.3, use {@link InMemoryUser} instead */ -final class User implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface +class User implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface { private $username; private $password; @@ -35,6 +33,10 @@ final class User implements UserInterface, PasswordAuthenticatedUserInterface, E public function __construct(?string $username, ?string $password, array $roles = [], bool $enabled = true, bool $userNonExpired = true, bool $credentialsNonExpired = true, bool $userNonLocked = true, array $extraFields = []) { + if (InMemoryUser::class !== static::class) { + trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', self::class, InMemoryUser::class); + } + if ('' === $username || null === $username) { throw new \InvalidArgumentException('The username cannot be empty.'); } @@ -175,8 +177,8 @@ public function isEqualTo(UserInterface $user): bool return false; } - $currentRoles = array_map('strval', (array)$this->getRoles()); - $newRoles = array_map('strval', (array)$user->getRoles()); + $currentRoles = array_map('strval', (array) $this->getRoles()); + $newRoles = array_map('strval', (array) $user->getRoles()); $rolesChanged = \count($currentRoles) !== \count($newRoles) || \count($currentRoles) !== \count(array_intersect($currentRoles, $newRoles)); if ($rolesChanged) { return false; @@ -186,16 +188,18 @@ public function isEqualTo(UserInterface $user): bool return false; } - if ($this->isAccountNonExpired() !== $user->isAccountNonExpired()) { - return false; - } + if (self::class === static::class) { + if ($this->isAccountNonExpired() !== $user->isAccountNonExpired()) { + return false; + } - if ($this->isAccountNonLocked() !== $user->isAccountNonLocked()) { - return false; - } + if ($this->isAccountNonLocked() !== $user->isAccountNonLocked()) { + return false; + } - if ($this->isCredentialsNonExpired() !== $user->isCredentialsNonExpired()) { - return false; + if ($this->isCredentialsNonExpired() !== $user->isCredentialsNonExpired()) { + return false; + } } if ($this->isEnabled() !== $user->isEnabled()) { From 20e21ef3692d8aebdbf7ac186f7e7f4c1f6b571f Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Wed, 10 Mar 2021 17:10:58 +0100 Subject: [PATCH 14/35] [Security] Rename UserInterface::getUsername() to getUserIdentifier() --- .../AuthenticationProviderManager.php | 6 ++ .../Provider/DaoAuthenticationProvider.php | 17 +++- .../LdapBindAuthenticationProvider.php | 32 ++++--- ...PreAuthenticatedAuthenticationProvider.php | 12 ++- .../RememberMeAuthenticationProvider.php | 2 +- .../Provider/UserAuthenticationProvider.php | 8 +- .../RememberMe/InMemoryTokenProvider.php | 2 +- Authentication/RememberMe/PersistentToken.php | 19 ++-- .../RememberMe/PersistentTokenInterface.php | 9 +- Authentication/Token/AbstractToken.php | 34 ++++++- Authentication/Token/NullToken.php | 7 ++ Authentication/Token/Storage/TokenStorage.php | 5 + Authentication/Token/TokenInterface.php | 9 +- Exception/UserNotFoundException.php | 96 +++++++++++++++++++ Exception/UsernameNotFoundException.php | 60 +----------- .../AuthenticationProviderManagerTest.php | 10 +- .../AuthenticationTrustResolverTest.php | 4 + .../DaoAuthenticationProviderTest.php | 79 ++++++++------- .../LdapBindAuthenticationProviderTest.php | 5 +- ...uthenticatedAuthenticationProviderTest.php | 5 +- .../UserAuthenticationProviderTest.php | 11 ++- .../RememberMe/PersistentTokenTest.php | 16 +++- .../Token/AbstractTokenTest.php | 57 +++++++++-- .../Storage/UsageTrackingTokenStorageTest.php | 3 +- .../Token/SwitchUserTokenTest.php | 11 ++- Tests/Encoder/EncoderFactoryTest.php | 4 + Tests/Exception/UserNotFoundExceptionTest.php | 39 ++++++++ .../UsernameNotFoundExceptionTest.php | 26 ----- Tests/User/ChainUserProviderTest.php | 61 ++++++------ Tests/User/InMemoryUserProviderTest.php | 10 +- Tests/User/InMemoryUserTest.php | 14 +++ Tests/User/UserTest.php | 14 +++ User/ChainUserProvider.php | 32 +++++-- User/InMemoryUserProvider.php | 34 ++++--- User/MissingUserProvider.php | 5 + User/User.php | 14 ++- User/UserInterface.php | 9 +- User/UserProviderInterface.php | 31 +++--- 38 files changed, 543 insertions(+), 269 deletions(-) create mode 100644 Exception/UserNotFoundException.php create mode 100644 Tests/Exception/UserNotFoundExceptionTest.php delete mode 100644 Tests/Exception/UsernameNotFoundExceptionTest.php diff --git a/Authentication/AuthenticationProviderManager.php b/Authentication/AuthenticationProviderManager.php index c4099603..ddf09830 100644 --- a/Authentication/AuthenticationProviderManager.php +++ b/Authentication/AuthenticationProviderManager.php @@ -21,6 +21,7 @@ use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\ProviderNotFoundException; +use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; // Help opcache.preload discover always-needed symbols @@ -105,6 +106,11 @@ public function authenticate(TokenInterface $token) $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($result), AuthenticationEvents::AUTHENTICATION_SUCCESS); } + // @deprecated since 5.3 + if ($user = $result->getUser() instanceof UserInterface && !method_exists($result->getUser(), 'getUserIdentifier')) { + trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "getUserIdentifier(): string" in user class "%s" is deprecated. This method will replace "getUsername()" in Symfony 6.0.', get_debug_type($result->getUser())); + } + return $result; } diff --git a/Authentication/Provider/DaoAuthenticationProvider.php b/Authentication/Provider/DaoAuthenticationProvider.php index eca9357f..4ef55664 100644 --- a/Authentication/Provider/DaoAuthenticationProvider.php +++ b/Authentication/Provider/DaoAuthenticationProvider.php @@ -16,7 +16,7 @@ use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; @@ -108,7 +108,7 @@ protected function checkAuthentication(UserInterface $user, UsernamePasswordToke /** * {@inheritdoc} */ - protected function retrieveUser(string $username, UsernamePasswordToken $token) + protected function retrieveUser(string $userIdentifier, UsernamePasswordToken $token) { $user = $token->getUser(); if ($user instanceof UserInterface) { @@ -116,15 +116,22 @@ protected function retrieveUser(string $username, UsernamePasswordToken $token) } try { - $user = $this->userProvider->loadUserByUsername($username); + // @deprecated since 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 + if (method_exists($this->userProvider, 'loadUserByIdentifier')) { + $user = $this->userProvider->loadUserByIdentifier($userIdentifier); + } else { + trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "loadUserByIdentifier()" in user provider "%s" is deprecated. This method will replace "loadUserByUsername()" in Symfony 6.0.', get_debug_type($this->userProvider)); + + $user = $this->userProvider->loadUserByUsername($userIdentifier); + } if (!$user instanceof UserInterface) { throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); } return $user; - } catch (UsernameNotFoundException $e) { - $e->setUsername($username); + } catch (UserNotFoundException $e) { + $e->setUserIdentifier($userIdentifier); throw $e; } catch (\Exception $e) { $e = new AuthenticationServiceException($e->getMessage(), 0, $e); diff --git a/Authentication/Provider/LdapBindAuthenticationProvider.php b/Authentication/Provider/LdapBindAuthenticationProvider.php index 3705ae82..e9a3ab02 100644 --- a/Authentication/Provider/LdapBindAuthenticationProvider.php +++ b/Authentication/Provider/LdapBindAuthenticationProvider.php @@ -16,7 +16,7 @@ use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\LogicException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -38,7 +38,7 @@ class LdapBindAuthenticationProvider extends UserAuthenticationProvider private $searchDn; private $searchPassword; - public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, string $providerKey, LdapInterface $ldap, string $dnString = '{username}', bool $hideUserNotFoundExceptions = true, string $searchDn = '', string $searchPassword = '') + public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, string $providerKey, LdapInterface $ldap, string $dnString = '{user_identifier}', bool $hideUserNotFoundExceptions = true, string $searchDn = '', string $searchPassword = '') { parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions); @@ -50,7 +50,7 @@ public function __construct(UserProviderInterface $userProvider, UserCheckerInte } /** - * Set a query string to use in order to find a DN for the username. + * Set a query string to use in order to find a DN for the user identifier. */ public function setQueryString(string $queryString) { @@ -60,13 +60,20 @@ public function setQueryString(string $queryString) /** * {@inheritdoc} */ - protected function retrieveUser(string $username, UsernamePasswordToken $token) + protected function retrieveUser(string $userIdentifier, UsernamePasswordToken $token) { - if (AuthenticationProviderInterface::USERNAME_NONE_PROVIDED === $username) { - throw new UsernameNotFoundException('Username can not be null.'); + if (AuthenticationProviderInterface::USERNAME_NONE_PROVIDED === $userIdentifier) { + throw new UserNotFoundException('User identifier can not be null.'); } - return $this->userProvider->loadUserByUsername($username); + // @deprecated since 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 + if (method_exists($this->userProvider, 'loadUserByIdentifier')) { + return $this->userProvider->loadUserByIdentifier($userIdentifier); + } else { + trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "loadUserByIdentifier()" in user provider "%s" is deprecated. This method will replace "loadUserByUsername()" in Symfony 6.0.', get_debug_type($this->userProvider)); + + return $this->userProvider->loadUserByUsername($userIdentifier); + } } /** @@ -74,7 +81,8 @@ protected function retrieveUser(string $username, UsernamePasswordToken $token) */ protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token) { - $username = $token->getUsername(); + // @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0 + $userIdentifier = method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername(); $password = $token->getCredentials(); if ('' === (string) $password) { @@ -88,8 +96,8 @@ protected function checkAuthentication(UserInterface $user, UsernamePasswordToke } else { throw new LogicException('Using the "query_string" config without using a "search_dn" and a "search_password" is not supported.'); } - $username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_FILTER); - $query = str_replace('{username}', $username, $this->queryString); + $userIdentifier = $this->ldap->escape($userIdentifier, '', LdapInterface::ESCAPE_FILTER); + $query = str_replace(['{username}', '{user_identifier}'], $userIdentifier, $this->queryString); $result = $this->ldap->query($this->dnString, $query)->execute(); if (1 !== $result->count()) { throw new BadCredentialsException('The presented username is invalid.'); @@ -97,8 +105,8 @@ protected function checkAuthentication(UserInterface $user, UsernamePasswordToke $dn = $result[0]->getDn(); } else { - $username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_DN); - $dn = str_replace('{username}', $username, $this->dnString); + $userIdentifier = $this->ldap->escape($userIdentifier, '', LdapInterface::ESCAPE_DN); + $dn = str_replace(['{username}', '{user_identifier}'], $userIdentifier, $this->dnString); } $this->ldap->bind($dn, $password); diff --git a/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php b/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php index c0612bc0..292b8b9f 100644 --- a/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php +++ b/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php @@ -24,7 +24,7 @@ * This authentication provider will not perform any checks on authentication * requests, as they should already be pre-authenticated. However, the * UserProviderInterface implementation may still throw a - * UsernameNotFoundException, for example. + * UserNotFoundException, for example. * * @author Fabien Potencier */ @@ -54,7 +54,15 @@ public function authenticate(TokenInterface $token) throw new BadCredentialsException('No pre-authenticated principal found in request.'); } - $user = $this->userProvider->loadUserByUsername($user); + $userIdentifier = method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername(); + // @deprecated since 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 + if (method_exists($this->userProvider, 'loadUserByIdentifier')) { + $user = $this->userProvider->loadUserByIdentifier($userIdentifier); + } else { + trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "loadUserByIdentifier()" in user provider "%s" is deprecated. This method will replace "loadUserByUsername()" in Symfony 6.0.', get_debug_type($this->userProvider)); + + $user = $this->userProvider->loadUserByUsername($userIdentifier); + } $this->userChecker->checkPostAuth($user); diff --git a/Authentication/Provider/RememberMeAuthenticationProvider.php b/Authentication/Provider/RememberMeAuthenticationProvider.php index 630064af..8ee8109b 100644 --- a/Authentication/Provider/RememberMeAuthenticationProvider.php +++ b/Authentication/Provider/RememberMeAuthenticationProvider.php @@ -51,7 +51,7 @@ public function authenticate(TokenInterface $token) $user = $token->getUser(); - if (!$token->getUser() instanceof UserInterface) { + if (!$user instanceof UserInterface) { throw new LogicException(sprintf('Method "%s::getUser()" must return a "%s" instance, "%s" returned.', get_debug_type($token), UserInterface::class, get_debug_type($user))); } diff --git a/Authentication/Provider/UserAuthenticationProvider.php b/Authentication/Provider/UserAuthenticationProvider.php index 21c1787e..4dfff685 100644 --- a/Authentication/Provider/UserAuthenticationProvider.php +++ b/Authentication/Provider/UserAuthenticationProvider.php @@ -17,7 +17,7 @@ use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -55,18 +55,18 @@ public function authenticate(TokenInterface $token) throw new AuthenticationException('The token is not supported by this authentication provider.'); } - $username = $token->getUsername(); + $username = method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername(); if ('' === $username || null === $username) { $username = AuthenticationProviderInterface::USERNAME_NONE_PROVIDED; } try { $user = $this->retrieveUser($username, $token); - } catch (UsernameNotFoundException $e) { + } catch (UserNotFoundException $e) { if ($this->hideUserNotFoundExceptions) { throw new BadCredentialsException('Bad credentials.', 0, $e); } - $e->setUsername($username); + $e->setUserIdentifier($username); throw $e; } diff --git a/Authentication/RememberMe/InMemoryTokenProvider.php b/Authentication/RememberMe/InMemoryTokenProvider.php index a1b30443..571bbe02 100644 --- a/Authentication/RememberMe/InMemoryTokenProvider.php +++ b/Authentication/RememberMe/InMemoryTokenProvider.php @@ -45,7 +45,7 @@ public function updateToken(string $series, string $tokenValue, \DateTime $lastU $token = new PersistentToken( $this->tokens[$series]->getClass(), - $this->tokens[$series]->getUsername(), + method_exists($this->tokens[$series], 'getUserIdentifier') ? $this->tokens[$series]->getUserIdentifier() : $this->tokens[$series]->getUsername(), $series, $tokenValue, $lastUsed diff --git a/Authentication/RememberMe/PersistentToken.php b/Authentication/RememberMe/PersistentToken.php index 1f0e485c..b8337adf 100644 --- a/Authentication/RememberMe/PersistentToken.php +++ b/Authentication/RememberMe/PersistentToken.php @@ -19,18 +19,18 @@ final class PersistentToken implements PersistentTokenInterface { private $class; - private $username; + private $userIdentifier; private $series; private $tokenValue; private $lastUsed; - public function __construct(string $class, string $username, string $series, string $tokenValue, \DateTime $lastUsed) + public function __construct(string $class, string $userIdentifier, string $series, string $tokenValue, \DateTime $lastUsed) { if (empty($class)) { throw new \InvalidArgumentException('$class must not be empty.'); } - if ('' === $username) { - throw new \InvalidArgumentException('$username must not be empty.'); + if ('' === $userIdentifier) { + throw new \InvalidArgumentException('$userIdentifier must not be empty.'); } if (empty($series)) { throw new \InvalidArgumentException('$series must not be empty.'); @@ -40,7 +40,7 @@ public function __construct(string $class, string $username, string $series, str } $this->class = $class; - $this->username = $username; + $this->userIdentifier = $userIdentifier; $this->series = $series; $this->tokenValue = $tokenValue; $this->lastUsed = $lastUsed; @@ -59,7 +59,14 @@ public function getClass(): string */ public function getUsername(): string { - return $this->username; + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, use getUserIdentifier() instead.', __METHOD__); + + return $this->userIdentifier; + } + + public function getUserIdentifier(): string + { + return $this->userIdentifier; } /** diff --git a/Authentication/RememberMe/PersistentTokenInterface.php b/Authentication/RememberMe/PersistentTokenInterface.php index ba31ffa6..85c5bc38 100644 --- a/Authentication/RememberMe/PersistentTokenInterface.php +++ b/Authentication/RememberMe/PersistentTokenInterface.php @@ -15,6 +15,8 @@ * Interface to be implemented by persistent token classes (such as * Doctrine entities representing a remember-me token). * + * @method string getUserIdentifier() returns the identifier used to authenticate (e.g. their e-mailaddress or username) + * * @author Johannes M. Schmitt */ interface PersistentTokenInterface @@ -26,13 +28,6 @@ interface PersistentTokenInterface */ public function getClass(); - /** - * Returns the username. - * - * @return string - */ - public function getUsername(); - /** * Returns the series. * diff --git a/Authentication/Token/AbstractToken.php b/Authentication/Token/AbstractToken.php index 0083ae39..b7934137 100644 --- a/Authentication/Token/AbstractToken.php +++ b/Authentication/Token/AbstractToken.php @@ -51,10 +51,32 @@ public function getRoleNames(): array /** * {@inheritdoc} */ - public function getUsername() + public function getUsername(/* $legacy = true */) { + if (1 === func_num_args() && false === func_get_arg(0)) { + return null; + } + + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, use getUserIdentifier() instead.', __METHOD__); + + if ($this->user instanceof UserInterface) { + return method_exists($this->user, 'getUserIdentifier') ? $this->user->getUserIdentifier() : $this->user->getUsername(); + } + + return (string) $this->user; + } + + public function getUserIdentifier(): string + { + // method returns "null" in non-legacy mode if not overriden + $username = $this->getUsername(false); + if (null !== $username) { + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s::getUsername()" is deprecated, override "getUserIdentifier()" instead.', get_debug_type($this)); + } + if ($this->user instanceof UserInterface) { - return $this->user->getUsername(); + // @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0 + return method_exists($this->user, 'getUserIdentifier') ? $this->user->getUserIdentifier() : $this->user->getUsername(); } return (string) $this->user; @@ -234,7 +256,7 @@ public function __toString() $roles[] = $role; } - return sprintf('%s(user="%s", authenticated=%s, roles="%s")', $class, $this->getUsername(), json_encode($this->authenticated), implode(', ', $roles)); + return sprintf('%s(user="%s", authenticated=%s, roles="%s")', $class, $this->getUserIdentifier(), json_encode($this->authenticated), implode(', ', $roles)); } /** @@ -283,7 +305,11 @@ private function hasUserChanged(UserInterface $user): bool return true; } - if ($this->user->getUsername() !== $user->getUsername()) { + // @deprecated since Symfony 5.3, drop getUsername() in 6.0 + $userIdentifier = function ($user) { + return method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername(); + }; + if ($userIdentifier($this->user) !== $userIdentifier($user)) { return true; } diff --git a/Authentication/Token/NullToken.php b/Authentication/Token/NullToken.php index 589ad1b4..5c8a1c24 100644 --- a/Authentication/Token/NullToken.php +++ b/Authentication/Token/NullToken.php @@ -42,6 +42,13 @@ public function setUser($user) } public function getUsername() + { + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, use getUserIdentifier() instead.', __METHOD__); + + return ''; + } + + public function getUserIdentifier(): string { return ''; } diff --git a/Authentication/Token/Storage/TokenStorage.php b/Authentication/Token/Storage/TokenStorage.php index 850c05e7..1fc30bfc 100644 --- a/Authentication/Token/Storage/TokenStorage.php +++ b/Authentication/Token/Storage/TokenStorage.php @@ -48,6 +48,11 @@ public function setToken(TokenInterface $token = null) if ($token) { // ensure any initializer is called $this->getToken(); + + // @deprecated since 5.3 + if (!method_exists($token, 'getUserIdentifier')) { + trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "getUserIdentifier(): string" in token class "%s" is deprecated. This method will replace "getUsername()" in Symfony 6.0.', get_debug_type($token)); + } } $this->initializer = null; diff --git a/Authentication/Token/TokenInterface.php b/Authentication/Token/TokenInterface.php index ad48ec64..047f571a 100644 --- a/Authentication/Token/TokenInterface.php +++ b/Authentication/Token/TokenInterface.php @@ -16,6 +16,8 @@ /** * TokenInterface is the interface for the user authentication information. * + * @method string getUserIdentifier() returns the user identifier used during authentication (e.g. a user's e-mailaddress or username) + * * @author Fabien Potencier * @author Johannes M. Schmitt */ @@ -65,13 +67,6 @@ public function getUser(); */ public function setUser($user); - /** - * Returns the username. - * - * @return string - */ - public function getUsername(); - /** * Returns whether the user is authenticated or not. * diff --git a/Exception/UserNotFoundException.php b/Exception/UserNotFoundException.php new file mode 100644 index 00000000..d730f7d7 --- /dev/null +++ b/Exception/UserNotFoundException.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +/** + * UserNotFoundException is thrown if a User cannot be found for the given identifier. + * + * @author Fabien Potencier + * @author Alexander + */ +class UserNotFoundException extends AuthenticationException +{ + private $identifier; + + /** + * {@inheritdoc} + */ + public function getMessageKey() + { + return 'Username could not be found.'; + } + + /** + * Get the user identifier (e.g. username or e-mailaddress). + */ + public function getUserIdentifier(): string + { + return $this->identifier; + } + + /** + * @return string + * + * @deprecated + */ + public function getUsername() + { + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, use getUserIdentifier() instead.', __METHOD__); + + return $this->identifier; + } + + /** + * Set the user identifier (e.g. username or e-mailaddress). + */ + public function setUserIdentifier(string $identifier): void + { + $this->identifier = $identifier; + } + + /** + * @deprecated + */ + public function setUsername(string $username) + { + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, use getUserIdentifier() instead.', __METHOD__); + + $this->identifier = $username; + } + + /** + * {@inheritdoc} + */ + public function getMessageData() + { + return ['{{ username }}' => $this->identifier, '{{ user_identifier }}' => $this->identifier]; + } + + /** + * {@inheritdoc} + */ + public function __serialize(): array + { + return [$this->identifier, parent::__serialize()]; + } + + /** + * {@inheritdoc} + */ + public function __unserialize(array $data): void + { + [$this->identifier, $parentData] = $data; + $parentData = \is_array($parentData) ? $parentData : unserialize($parentData); + parent::__unserialize($parentData); + } +} +class_alias(UserNotFoundException::class, UsernameNotFoundException::class); diff --git a/Exception/UsernameNotFoundException.php b/Exception/UsernameNotFoundException.php index f4601323..e0d2d4a2 100644 --- a/Exception/UsernameNotFoundException.php +++ b/Exception/UsernameNotFoundException.php @@ -11,65 +11,15 @@ namespace Symfony\Component\Security\Core\Exception; -/** - * UsernameNotFoundException is thrown if a User cannot be found by its username. - * - * @author Fabien Potencier - * @author Alexander - */ -class UsernameNotFoundException extends AuthenticationException -{ - private $username; - - /** - * {@inheritdoc} - */ - public function getMessageKey() - { - return 'Username could not be found.'; - } - - /** - * Get the username. - * - * @return string - */ - public function getUsername() - { - return $this->username; - } - - /** - * Set the username. - */ - public function setUsername(string $username) - { - $this->username = $username; - } +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', UsernameNotFoundException::class, UserNotFoundException::class); - /** - * {@inheritdoc} - */ - public function getMessageData() - { - return ['{{ username }}' => $this->username]; - } - - /** - * {@inheritdoc} - */ - public function __serialize(): array - { - return [$this->username, parent::__serialize()]; - } +class_exists(UserNotFoundException::class); +if (false) { /** - * {@inheritdoc} + * @deprecated since Symfony 5.3 to be removed in 6.0, use UserNotFoundException instead. */ - public function __unserialize(array $data): void + class UsernameNotFoundException extends AuthenticationException { - [$this->username, $parentData] = $data; - $parentData = \is_array($parentData) ? $parentData : unserialize($parentData); - parent::__unserialize($parentData); } } diff --git a/Tests/Authentication/AuthenticationProviderManagerTest.php b/Tests/Authentication/AuthenticationProviderManagerTest.php index db1e3887..d41805bf 100644 --- a/Tests/Authentication/AuthenticationProviderManagerTest.php +++ b/Tests/Authentication/AuthenticationProviderManagerTest.php @@ -23,6 +23,7 @@ use Symfony\Component\Security\Core\Exception\AccountStatusException; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\ProviderNotFoundException; +use Symfony\Component\Security\Core\User\InMemoryUser; class AuthenticationProviderManagerTest extends TestCase { @@ -90,9 +91,12 @@ public function testAuthenticateWhenProviderReturnsAuthenticationException() public function testAuthenticateWhenOneReturnsAuthenticationExceptionButNotAll() { + $expected = $this->createMock(TokenInterface::class); + $expected->expects($this->any())->method('getUser')->willReturn(new InMemoryUser('wouter', null)); + $manager = new AuthenticationProviderManager([ $this->getAuthenticationProvider(true, null, AuthenticationException::class), - $this->getAuthenticationProvider(true, $expected = $this->createMock(TokenInterface::class)), + $this->getAuthenticationProvider(true, $expected), ]); $token = $manager->authenticate($this->createMock(TokenInterface::class)); @@ -106,8 +110,10 @@ public function testAuthenticateReturnsTokenOfTheFirstMatchingProvider() ->expects($this->never()) ->method('supports') ; + $expected = $this->createMock(TokenInterface::class); + $expected->expects($this->any())->method('getUser')->willReturn(new InMemoryUser('wouter', null)); $manager = new AuthenticationProviderManager([ - $this->getAuthenticationProvider(true, $expected = $this->createMock(TokenInterface::class)), + $this->getAuthenticationProvider(true, $expected), $second, ]); diff --git a/Tests/Authentication/AuthenticationTrustResolverTest.php b/Tests/Authentication/AuthenticationTrustResolverTest.php index cd1924c6..adb14975 100644 --- a/Tests/Authentication/AuthenticationTrustResolverTest.php +++ b/Tests/Authentication/AuthenticationTrustResolverTest.php @@ -155,6 +155,10 @@ public function getUsername(): string { } + public function getUserIdentifier(): string + { + } + public function isAuthenticated(): bool { } diff --git a/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php b/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php index 46b5624b..05340afa 100644 --- a/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\Provider; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher; use Symfony\Component\PasswordHasher\PasswordHasherInterface; @@ -20,8 +21,9 @@ use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -29,13 +31,25 @@ class DaoAuthenticationProviderTest extends TestCase { + use ExpectDeprecationTrait; + + /** + * @group legacy + */ public function testRetrieveUserWhenProviderDoesNotReturnAnUserInterface() { $this->expectException(AuthenticationServiceException::class); - $provider = $this->getProvider('fabien'); + $userProvider = $this->createMock(DaoAuthenticationProviderTest_UserProvider::class); + $userProvider->expects($this->once()) + ->method('loadUserByUsername') + ->willReturn('fabien') + ; + $provider = $this->getProvider(null, null, null, $userProvider); $method = new \ReflectionMethod($provider, 'retrieveUser'); $method->setAccessible(true); + $this->expectDeprecation('Since symfony/security-core 5.3: Not implementing method "loadUserByIdentifier()" in user provider "'.get_debug_type($userProvider).'" is deprecated. This method will replace "loadUserByUsername()" in Symfony 6.0.'); + $method->invoke($provider, 'fabien', $this->getSupportedToken()); } @@ -44,12 +58,8 @@ public function testRetrieveUserWhenProviderDoesNotReturnAnUserInterface() */ public function testRetrieveUserWhenUsernameIsNotFoundWithLegacyEncoderFactory() { - $this->expectException(UsernameNotFoundException::class); - $userProvider = $this->createMock(UserProviderInterface::class); - $userProvider->expects($this->once()) - ->method('loadUserByUsername') - ->willThrowException(new UsernameNotFoundException()) - ; + $this->expectException(UserNotFoundException::class); + $userProvider = new InMemoryUserProvider(); $provider = new DaoAuthenticationProvider($userProvider, $this->createMock(UserCheckerInterface::class), 'key', $this->createMock(EncoderFactoryInterface::class)); $method = new \ReflectionMethod($provider, 'retrieveUser'); @@ -60,12 +70,8 @@ public function testRetrieveUserWhenUsernameIsNotFoundWithLegacyEncoderFactory() public function testRetrieveUserWhenUsernameIsNotFound() { - $this->expectException(UsernameNotFoundException::class); - $userProvider = $this->createMock(UserProviderInterface::class); - $userProvider->expects($this->once()) - ->method('loadUserByUsername') - ->willThrowException(new UsernameNotFoundException()) - ; + $this->expectException(UserNotFoundException::class); + $userProvider = new InMemoryUserProvider(); $provider = new DaoAuthenticationProvider($userProvider, $this->createMock(UserCheckerInterface::class), 'key', $this->createMock(PasswordHasherFactoryInterface::class)); $method = new \ReflectionMethod($provider, 'retrieveUser'); @@ -77,9 +83,9 @@ public function testRetrieveUserWhenUsernameIsNotFound() public function testRetrieveUserWhenAnExceptionOccurs() { $this->expectException(AuthenticationServiceException::class); - $userProvider = $this->createMock(UserProviderInterface::class); + $userProvider = $this->createMock(InMemoryUserProvider::class); $userProvider->expects($this->once()) - ->method('loadUserByUsername') + ->method('loadUserByIdentifier') ->willThrowException(new \RuntimeException()) ; @@ -92,9 +98,9 @@ public function testRetrieveUserWhenAnExceptionOccurs() public function testRetrieveUserReturnsUserFromTokenOnReauthentication() { - $userProvider = $this->createMock(UserProviderInterface::class); + $userProvider = $this->createMock(InMemoryUserProvider::class); $userProvider->expects($this->never()) - ->method('loadUserByUsername') + ->method('loadUserByIdentifier') ; $user = new TestUser(); @@ -114,19 +120,13 @@ public function testRetrieveUserReturnsUserFromTokenOnReauthentication() public function testRetrieveUser() { - $user = new TestUser(); - - $userProvider = $this->createMock(UserProviderInterface::class); - $userProvider->expects($this->once()) - ->method('loadUserByUsername') - ->willReturn($user) - ; + $userProvider = new InMemoryUserProvider(['fabien' => []]); $provider = new DaoAuthenticationProvider($userProvider, $this->createMock(UserCheckerInterface::class), 'key', $this->createMock(PasswordHasherFactoryInterface::class)); $method = new \ReflectionMethod($provider, 'retrieveUser'); $method->setAccessible(true); - $this->assertSame($user, $method->invoke($provider, 'fabien', $this->getSupportedToken())); + $this->assertEquals('fabien', $method->invoke($provider, 'fabien', $this->getSupportedToken())->getUserIdentifier()); } public function testCheckAuthenticationWhenCredentialsAreEmpty() @@ -323,14 +323,16 @@ protected function getSupportedToken() return $mock; } - protected function getProvider($user = null, $userChecker = null, $passwordHasher = null) + protected function getProvider($user = null, $userChecker = null, $passwordHasher = null, $userProvider = null) { - $userProvider = $this->createMock(PasswordUpgraderProvider::class); - if (null !== $user) { - $userProvider->expects($this->once()) - ->method('loadUserByUsername') - ->willReturn($user) - ; + if (null === $userProvider) { + $userProvider = $this->createMock(PasswordUpgraderProvider::class); + if (null !== $user) { + $userProvider->expects($this->once()) + ->method('loadUserByIdentifier') + ->willReturn($user) + ; + } } if (null === $userChecker) { @@ -374,6 +376,11 @@ public function getUsername(): string return 'jane_doe'; } + public function getUserIdentifier(): string + { + return 'jane_doe'; + } + public function eraseCredentials() { } @@ -381,4 +388,10 @@ public function eraseCredentials() interface PasswordUpgraderProvider extends UserProviderInterface, PasswordUpgraderInterface { public function upgradePassword(UserInterface $user, string $newHashedPassword): void; + public function loadUserByIdentifier(string $identifier): UserInterface; +} + +interface DaoAuthenticationProviderTest_UserProvider extends UserProviderInterface +{ + public function loadUserByUsername($username); } diff --git a/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php b/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php index c4750844..4507e6a9 100644 --- a/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php @@ -21,6 +21,7 @@ use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -81,10 +82,10 @@ public function testBindFailureShouldThrowAnException() public function testRetrieveUser() { - $userProvider = $this->createMock(UserProviderInterface::class); + $userProvider = $this->createMock(InMemoryUserProvider::class); $userProvider ->expects($this->once()) - ->method('loadUserByUsername') + ->method('loadUserByIdentifier') ->with('foo') ; $ldap = $this->createMock(LdapInterface::class); diff --git a/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php b/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php index a0d60413..15c079b8 100644 --- a/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\LockedException; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -120,10 +121,10 @@ protected function getSupportedToken($user = false, $credentials = false) protected function getProvider($user = null, $userChecker = null) { - $userProvider = $this->createMock(UserProviderInterface::class); + $userProvider = $this->createMock(InMemoryUserProvider::class); if (null !== $user) { $userProvider->expects($this->once()) - ->method('loadUserByUsername') + ->method('loadUserByIdentifier') ->willReturn($user) ; } diff --git a/Tests/Authentication/Provider/UserAuthenticationProviderTest.php b/Tests/Authentication/Provider/UserAuthenticationProviderTest.php index f006d37e..92b71448 100644 --- a/Tests/Authentication/Provider/UserAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/UserAuthenticationProviderTest.php @@ -21,7 +21,8 @@ use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\CredentialsExpiredException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; +use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -46,11 +47,11 @@ public function testAuthenticateWhenTokenIsNotSupported() public function testAuthenticateWhenUsernameIsNotFound() { - $this->expectException(UsernameNotFoundException::class); + $this->expectException(UserNotFoundException::class); $provider = $this->getProvider(false, false); $provider->expects($this->once()) ->method('retrieveUser') - ->willThrowException(new UsernameNotFoundException()) + ->willThrowException(new UserNotFoundException()) ; $provider->authenticate($this->getSupportedToken()); @@ -62,7 +63,7 @@ public function testAuthenticateWhenUsernameIsNotFoundAndHideIsTrue() $provider = $this->getProvider(false, true); $provider->expects($this->once()) ->method('retrieveUser') - ->willThrowException(new UsernameNotFoundException()) + ->willThrowException(new UserNotFoundException()) ; $provider->authenticate($this->getSupportedToken()); @@ -194,7 +195,7 @@ public function testAuthenticatePreservesOriginalToken() ; $originalToken = $this->createMock(TokenInterface::class); - $token = new SwitchUserToken($this->createMock(UserInterface::class), 'foo', 'key', [], $originalToken); + $token = new SwitchUserToken(new InMemoryUser('wouter', null), 'foo', 'key', [], $originalToken); $token->setAttributes(['foo' => 'bar']); $authToken = $provider->authenticate($token); diff --git a/Tests/Authentication/RememberMe/PersistentTokenTest.php b/Tests/Authentication/RememberMe/PersistentTokenTest.php index 12c133f5..9df545a4 100644 --- a/Tests/Authentication/RememberMe/PersistentTokenTest.php +++ b/Tests/Authentication/RememberMe/PersistentTokenTest.php @@ -12,19 +12,33 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\RememberMe; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken; class PersistentTokenTest extends TestCase { + use ExpectDeprecationTrait; + public function testConstructor() { $lastUsed = new \DateTime(); $token = new PersistentToken('fooclass', 'fooname', 'fooseries', 'footokenvalue', $lastUsed); $this->assertEquals('fooclass', $token->getClass()); - $this->assertEquals('fooname', $token->getUsername()); + $this->assertEquals('fooname', $token->getUserIdentifier()); $this->assertEquals('fooseries', $token->getSeries()); $this->assertEquals('footokenvalue', $token->getTokenValue()); $this->assertSame($lastUsed, $token->getLastUsed()); } + + /** + * @group legacy + */ + public function testLegacyGetUsername() + { + $token = new PersistentToken('fooclass', 'fooname', 'fooseries', 'footokenvalue', new \DateTime()); + + $this->expectDeprecation('Since symfony/security-core 5.3: Method "Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken::getUsername()" is deprecated, use getUserIdentifier() instead.'); + $this->assertEquals('fooname', $token->getUsername()); + } } diff --git a/Tests/Authentication/Token/AbstractTokenTest.php b/Tests/Authentication/Token/AbstractTokenTest.php index 98f84e1f..dcf479c8 100644 --- a/Tests/Authentication/Token/AbstractTokenTest.php +++ b/Tests/Authentication/Token/AbstractTokenTest.php @@ -12,12 +12,19 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\Token; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; +use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\UserInterface; class AbstractTokenTest extends TestCase { - public function testGetUsername() + use ExpectDeprecationTrait; + + /** + * @group legacy + */ + public function testLegacyGetUsername() { $token = new ConcreteToken(['ROLE_FOO']); $token->setUser('fabien'); @@ -26,10 +33,43 @@ public function testGetUsername() $token->setUser(new TestUser('fabien')); $this->assertEquals('fabien', $token->getUsername()); - $user = $this->createMock(UserInterface::class); - $user->expects($this->once())->method('getUsername')->willReturn('fabien'); - $token->setUser($user); + $legacyUser = new class implements UserInterface { + public function getUsername() + { + return 'fabien'; + } + + public function getRoles() + {} + + public function getPassword() + {} + + public function getSalt() + {} + + public function eraseCredentials() + {} + }; + $token->setUser($legacyUser); $this->assertEquals('fabien', $token->getUsername()); + + $token->setUser($legacyUser); + $this->assertEquals('fabien', $token->getUserIdentifier()); + } + + public function testGetUserIdentifier() + { + $token = new ConcreteToken(['ROLE_FOO']); + $token->setUser('fabien'); + $this->assertEquals('fabien', $token->getUserIdentifier()); + + $token->setUser(new TestUser('fabien')); + $this->assertEquals('fabien', $token->getUserIdentifier()); + + $user = new InMemoryUser('fabien', null); + $token->setUser($user); + $this->assertEquals('fabien', $token->getUserIdentifier()); } public function testEraseCredentials() @@ -106,10 +146,8 @@ public function testSetUser($user) public function getUsers() { - $user = $this->createMock(UserInterface::class); - return [ - [$user], + [new InMemoryUser('foo', null)], [new TestUser('foo')], ['foo'], ]; @@ -210,6 +248,11 @@ public function getUsername() return $this->name; } + public function getUserIdentifier() + { + return $this->name; + } + public function getPassword() { return '***'; diff --git a/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php b/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php index c5d2eaf5..38806efa 100644 --- a/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php +++ b/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\Security\Core\Authentication\Token\NullToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; @@ -46,7 +47,7 @@ public function testGetSetToken() $trackingStorage = new UsageTrackingTokenStorage($tokenStorage, $sessionLocator); $this->assertNull($trackingStorage->getToken()); - $token = $this->createMock(TokenInterface::class); + $token = new NullToken(); $trackingStorage->setToken($token); $this->assertSame($token, $trackingStorage->getToken()); diff --git a/Tests/Authentication/Token/SwitchUserTokenTest.php b/Tests/Authentication/Token/SwitchUserTokenTest.php index 8138f765..477247e7 100644 --- a/Tests/Authentication/Token/SwitchUserTokenTest.php +++ b/Tests/Authentication/Token/SwitchUserTokenTest.php @@ -26,7 +26,7 @@ public function testSerialize() $unserializedToken = unserialize(serialize($token)); $this->assertInstanceOf(SwitchUserToken::class, $unserializedToken); - $this->assertSame('admin', $unserializedToken->getUsername()); + $this->assertSame('admin', $unserializedToken->getUserIdentifier()); $this->assertSame('bar', $unserializedToken->getCredentials()); $this->assertSame('provider-key', $unserializedToken->getFirewallName()); $this->assertEquals(['ROLE_USER'], $unserializedToken->getRoleNames()); @@ -35,7 +35,7 @@ public function testSerialize() $unserializedOriginalToken = $unserializedToken->getOriginalToken(); $this->assertInstanceOf(UsernamePasswordToken::class, $unserializedOriginalToken); - $this->assertSame('user', $unserializedOriginalToken->getUsername()); + $this->assertSame('user', $unserializedOriginalToken->getUserIdentifier()); $this->assertSame('foo', $unserializedOriginalToken->getCredentials()); $this->assertSame('provider-key', $unserializedOriginalToken->getFirewallName()); $this->assertEquals(['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH'], $unserializedOriginalToken->getRoleNames()); @@ -49,6 +49,11 @@ public function getUsername() return 'impersonated'; } + public function getUserIdentifier() + { + return 'impersonated'; + } + public function getPassword() { return null; @@ -92,7 +97,7 @@ public function testUnserializeOldToken() self::assertInstanceOf(SwitchUserToken::class, $token); self::assertInstanceOf(UsernamePasswordToken::class, $token->getOriginalToken()); - self::assertSame('john', $token->getUsername()); + self::assertSame('john', $token->getUserIdentifier()); self::assertSame(['foo' => 'bar'], $token->getCredentials()); self::assertSame('main', $token->getFirewallName()); self::assertEquals(['ROLE_USER'], $token->getRoleNames()); diff --git a/Tests/Encoder/EncoderFactoryTest.php b/Tests/Encoder/EncoderFactoryTest.php index 7b79986b..3744e05b 100644 --- a/Tests/Encoder/EncoderFactoryTest.php +++ b/Tests/Encoder/EncoderFactoryTest.php @@ -213,6 +213,10 @@ public function getUsername(): string { } + public function getUserIdentifier(): string + { + } + public function eraseCredentials() { } diff --git a/Tests/Exception/UserNotFoundExceptionTest.php b/Tests/Exception/UserNotFoundExceptionTest.php new file mode 100644 index 00000000..559e62ac --- /dev/null +++ b/Tests/Exception/UserNotFoundExceptionTest.php @@ -0,0 +1,39 @@ + + * + * 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\Exception; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; + +class UserNotFoundExceptionTest extends TestCase +{ + public function testGetMessageData() + { + $exception = new UserNotFoundException('Username could not be found.'); + $this->assertEquals(['{{ username }}' => null, '{{ user_identifier }}' => null], $exception->getMessageData()); + $exception->setUserIdentifier('username'); + $this->assertEquals(['{{ username }}' => 'username', '{{ user_identifier }}' => 'username'], $exception->getMessageData()); + } + + /** + * @group legacy + */ + public function testUsernameNotFoundException() + { + $exception = new UsernameNotFoundException(); + $this->assertInstanceOf(UserNotFoundException::class, $exception); + + $exception->setUsername('username'); + $this->assertEquals('username', $exception->getUserIdentifier()); + } +} diff --git a/Tests/Exception/UsernameNotFoundExceptionTest.php b/Tests/Exception/UsernameNotFoundExceptionTest.php deleted file mode 100644 index 8e256aac..00000000 --- a/Tests/Exception/UsernameNotFoundExceptionTest.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * 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\Exception; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; - -class UsernameNotFoundExceptionTest extends TestCase -{ - public function testGetMessageData() - { - $exception = new UsernameNotFoundException('Username could not be found.'); - $this->assertEquals(['{{ username }}' => null], $exception->getMessageData()); - $exception->setUsername('username'); - $this->assertEquals(['{{ username }}' => 'username'], $exception->getMessageData()); - } -} diff --git a/Tests/User/ChainUserProviderTest.php b/Tests/User/ChainUserProviderTest.php index 74d0cc13..5a477006 100644 --- a/Tests/User/ChainUserProviderTest.php +++ b/Tests/User/ChainUserProviderTest.php @@ -13,9 +13,10 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\User\ChainUserProvider; use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -25,59 +26,59 @@ class ChainUserProviderTest extends TestCase { public function testLoadUserByUsername() { - $provider1 = $this->createMock(UserProviderInterface::class); + $provider1 = $this->createMock(InMemoryUserProvider::class); $provider1 ->expects($this->once()) - ->method('loadUserByUsername') + ->method('loadUserByIdentifier') ->with($this->equalTo('foo')) - ->willThrowException(new UsernameNotFoundException('not found')) + ->willThrowException(new UserNotFoundException('not found')) ; - $provider2 = $this->createMock(UserProviderInterface::class); + $provider2 = $this->createMock(InMemoryUserProvider::class); $provider2 ->expects($this->once()) - ->method('loadUserByUsername') + ->method('loadUserByIdentifier') ->with($this->equalTo('foo')) ->willReturn($account = $this->createMock(UserInterface::class)) ; $provider = new ChainUserProvider([$provider1, $provider2]); - $this->assertSame($account, $provider->loadUserByUsername('foo')); + $this->assertSame($account, $provider->loadUserByIdentifier('foo')); } - public function testLoadUserByUsernameThrowsUsernameNotFoundException() + public function testLoadUserByUsernameThrowsUserNotFoundException() { - $this->expectException(UsernameNotFoundException::class); - $provider1 = $this->createMock(UserProviderInterface::class); + $this->expectException(UserNotFoundException::class); + $provider1 = $this->createMock(InMemoryUserProvider::class); $provider1 ->expects($this->once()) - ->method('loadUserByUsername') + ->method('loadUserByIdentifier') ->with($this->equalTo('foo')) - ->willThrowException(new UsernameNotFoundException('not found')) + ->willThrowException(new UserNotFoundException('not found')) ; - $provider2 = $this->createMock(UserProviderInterface::class); + $provider2 = $this->createMock(InMemoryUserProvider::class); $provider2 ->expects($this->once()) - ->method('loadUserByUsername') + ->method('loadUserByIdentifier') ->with($this->equalTo('foo')) - ->willThrowException(new UsernameNotFoundException('not found')) + ->willThrowException(new UserNotFoundException('not found')) ; $provider = new ChainUserProvider([$provider1, $provider2]); - $provider->loadUserByUsername('foo'); + $provider->loadUserByIdentifier('foo'); } public function testRefreshUser() { - $provider1 = $this->createMock(UserProviderInterface::class); + $provider1 = $this->createMock(InMemoryUserProvider::class); $provider1 ->expects($this->once()) ->method('supportsClass') ->willReturn(false) ; - $provider2 = $this->createMock(UserProviderInterface::class); + $provider2 = $this->createMock(InMemoryUserProvider::class); $provider2 ->expects($this->once()) ->method('supportsClass') @@ -90,7 +91,7 @@ public function testRefreshUser() ->willThrowException(new UnsupportedUserException('unsupported')) ; - $provider3 = $this->createMock(UserProviderInterface::class); + $provider3 = $this->createMock(InMemoryUserProvider::class); $provider3 ->expects($this->once()) ->method('supportsClass') @@ -109,7 +110,7 @@ public function testRefreshUser() public function testRefreshUserAgain() { - $provider1 = $this->createMock(UserProviderInterface::class); + $provider1 = $this->createMock(InMemoryUserProvider::class); $provider1 ->expects($this->once()) ->method('supportsClass') @@ -119,10 +120,10 @@ public function testRefreshUserAgain() $provider1 ->expects($this->once()) ->method('refreshUser') - ->willThrowException(new UsernameNotFoundException('not found')) + ->willThrowException(new UserNotFoundException('not found')) ; - $provider2 = $this->createMock(UserProviderInterface::class); + $provider2 = $this->createMock(InMemoryUserProvider::class); $provider2 ->expects($this->once()) ->method('supportsClass') @@ -142,7 +143,7 @@ public function testRefreshUserAgain() public function testRefreshUserThrowsUnsupportedUserException() { $this->expectException(UnsupportedUserException::class); - $provider1 = $this->createMock(UserProviderInterface::class); + $provider1 = $this->createMock(InMemoryUserProvider::class); $provider1 ->expects($this->once()) ->method('supportsClass') @@ -155,7 +156,7 @@ public function testRefreshUserThrowsUnsupportedUserException() ->willThrowException(new UnsupportedUserException('unsupported')) ; - $provider2 = $this->createMock(UserProviderInterface::class); + $provider2 = $this->createMock(InMemoryUserProvider::class); $provider2 ->expects($this->once()) ->method('supportsClass') @@ -174,7 +175,7 @@ public function testRefreshUserThrowsUnsupportedUserException() public function testSupportsClass() { - $provider1 = $this->createMock(UserProviderInterface::class); + $provider1 = $this->createMock(InMemoryUserProvider::class); $provider1 ->expects($this->once()) ->method('supportsClass') @@ -182,7 +183,7 @@ public function testSupportsClass() ->willReturn(false) ; - $provider2 = $this->createMock(UserProviderInterface::class); + $provider2 = $this->createMock(InMemoryUserProvider::class); $provider2 ->expects($this->once()) ->method('supportsClass') @@ -196,7 +197,7 @@ public function testSupportsClass() public function testSupportsClassWhenNotSupported() { - $provider1 = $this->createMock(UserProviderInterface::class); + $provider1 = $this->createMock(InMemoryUserProvider::class); $provider1 ->expects($this->once()) ->method('supportsClass') @@ -204,7 +205,7 @@ public function testSupportsClassWhenNotSupported() ->willReturn(false) ; - $provider2 = $this->createMock(UserProviderInterface::class); + $provider2 = $this->createMock(InMemoryUserProvider::class); $provider2 ->expects($this->once()) ->method('supportsClass') @@ -218,7 +219,7 @@ public function testSupportsClassWhenNotSupported() public function testAcceptsTraversable() { - $provider1 = $this->createMock(UserProviderInterface::class); + $provider1 = $this->createMock(InMemoryUserProvider::class); $provider1 ->expects($this->once()) ->method('supportsClass') @@ -231,7 +232,7 @@ public function testAcceptsTraversable() ->willThrowException(new UnsupportedUserException('unsupported')) ; - $provider2 = $this->createMock(UserProviderInterface::class); + $provider2 = $this->createMock(InMemoryUserProvider::class); $provider2 ->expects($this->once()) ->method('supportsClass') diff --git a/Tests/User/InMemoryUserProviderTest.php b/Tests/User/InMemoryUserProviderTest.php index f9d27c8f..d4d4964c 100644 --- a/Tests/User/InMemoryUserProviderTest.php +++ b/Tests/User/InMemoryUserProviderTest.php @@ -13,7 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Core\User\User; @@ -26,7 +26,7 @@ public function testConstructor() { $provider = $this->createProvider(); - $user = $provider->loadUserByUsername('fabien'); + $user = $provider->loadUserByIdentifier('fabien'); $this->assertEquals('foo', $user->getPassword()); $this->assertEquals(['ROLE_USER'], $user->getRoles()); $this->assertFalse($user->isEnabled()); @@ -76,7 +76,7 @@ public function testCreateUser() $provider = new InMemoryUserProvider(); $provider->createUser(new InMemoryUser('fabien', 'foo')); - $user = $provider->loadUserByUsername('fabien'); + $user = $provider->loadUserByIdentifier('fabien'); $this->assertEquals('foo', $user->getPassword()); } @@ -90,8 +90,8 @@ public function testCreateUserAlreadyExist() public function testLoadUserByUsernameDoesNotExist() { - $this->expectException(UsernameNotFoundException::class); + $this->expectException(UserNotFoundException::class); $provider = new InMemoryUserProvider(); - $provider->loadUserByUsername('fabien'); + $provider->loadUserByIdentifier('fabien'); } } diff --git a/Tests/User/InMemoryUserTest.php b/Tests/User/InMemoryUserTest.php index 885d1f73..a5496ef3 100644 --- a/Tests/User/InMemoryUserTest.php +++ b/Tests/User/InMemoryUserTest.php @@ -12,12 +12,15 @@ namespace Symfony\Component\Security\Core\Tests\User; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Security\Core\User\EquatableInterface; use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\UserInterface; class InMemoryUserTest extends TestCase { + use ExpectDeprecationTrait; + public function testConstructorException() { $this->expectException(\InvalidArgumentException::class); @@ -39,12 +42,23 @@ public function testGetPassword() $this->assertEquals('superpass', $user->getPassword()); } + /** + * @group legacy + */ public function testGetUsername() { $user = new InMemoryUser('fabien', 'superpass'); + + $this->expectDeprecation('Since symfony/security-core 5.3: Method "Symfony\Component\Security\Core\User\User::getUsername()" is deprecated, use getUserIdentifier() instead.'); $this->assertEquals('fabien', $user->getUsername()); } + public function testGetUserIdentifier() + { + $user = new InMemoryUser('fabien', 'superpass'); + $this->assertEquals('fabien', $user->getUserIdentifier()); + } + public function testGetSalt() { $user = new InMemoryUser('fabien', 'superpass'); diff --git a/Tests/User/UserTest.php b/Tests/User/UserTest.php index 143479de..81b8705d 100644 --- a/Tests/User/UserTest.php +++ b/Tests/User/UserTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Core\Tests\User; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Security\Core\User\EquatableInterface; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserInterface; @@ -21,6 +22,8 @@ */ class UserTest extends TestCase { + use ExpectDeprecationTrait; + public function testConstructorException() { $this->expectException(\InvalidArgumentException::class); @@ -42,12 +45,23 @@ public function testGetPassword() $this->assertEquals('superpass', $user->getPassword()); } + /** + * @group legacy + */ public function testGetUsername() { $user = new User('fabien', 'superpass'); + + $this->expectDeprecation('Since symfony/security-core 5.3: Method "Symfony\Component\Security\Core\User\User::getUsername()" is deprecated, use getUserIdentifier() instead.'); $this->assertEquals('fabien', $user->getUsername()); } + public function testGetUserIdentifier() + { + $user = new User('fabien', 'superpass'); + $this->assertEquals('fabien', $user->getUserIdentifier()); + } + public function testGetSalt() { $user = new User('fabien', 'superpass'); diff --git a/User/ChainUserProvider.php b/User/ChainUserProvider.php index fedcdb6a..35207d62 100644 --- a/User/ChainUserProvider.php +++ b/User/ChainUserProvider.php @@ -12,7 +12,7 @@ namespace Symfony\Component\Security\Core\User; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; /** * Chain User Provider. @@ -50,17 +50,31 @@ public function getProviders() * {@inheritdoc} */ public function loadUserByUsername(string $username) + { + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, use loadUserByIdentifier() instead.', __METHOD__); + + return $this->loadUserByIdentifier($username); + } + + public function loadUserByIdentifier(string $userIdentifier): UserInterface { foreach ($this->providers as $provider) { try { - return $provider->loadUserByUsername($username); - } catch (UsernameNotFoundException $e) { + // @deprecated since 5.3, change to $provider->loadUserByIdentifier() in 6.0 + if (!method_exists($provider, 'loadUserByIdentifier')) { + trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "loadUserByIdentifier()" in user provider "%s" is deprecated. This method will replace "loadUserByUsername()" in Symfony 6.0.', \get_debug_type($provider)); + + return $provider->loadUserByUsername($userIdentifier); + } + + return $provider->loadUserByIdentifier($userIdentifier); + } catch (UserNotFoundException $e) { // try next one } } - $ex = new UsernameNotFoundException(sprintf('There is no user with name "%s".', $username)); - $ex->setUsername($username); + $ex = new UserNotFoundException(sprintf('There is no user with identifier "%s".', $userIdentifier)); + $ex->setUserIdentifier($userIdentifier); throw $ex; } @@ -80,15 +94,17 @@ public function refreshUser(UserInterface $user) return $provider->refreshUser($user); } catch (UnsupportedUserException $e) { // try next one - } catch (UsernameNotFoundException $e) { + } catch (UserNotFoundException $e) { $supportedUserFound = true; // try next one } } if ($supportedUserFound) { - $e = new UsernameNotFoundException(sprintf('There is no user with name "%s".', $user->getUsername())); - $e->setUsername($user->getUsername()); + // @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0 + $username = method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername(); + $e = new UserNotFoundException(sprintf('There is no user with name "%s".', $username)); + $e->setUserIdentifier($username); throw $e; } else { throw new UnsupportedUserException(sprintf('There is no user provider for user "%s". Shouldn\'t the "supportsClass()" method of your user provider return true for this classname?', get_debug_type($user))); diff --git a/User/InMemoryUserProvider.php b/User/InMemoryUserProvider.php index c79f96e2..2e9ea5a2 100644 --- a/User/InMemoryUserProvider.php +++ b/User/InMemoryUserProvider.php @@ -12,7 +12,7 @@ namespace Symfony\Component\Security\Core\User; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; /** * InMemoryUserProvider is a simple non persistent user provider. @@ -51,11 +51,13 @@ public function __construct(array $users = []) */ public function createUser(UserInterface $user) { - if (isset($this->users[strtolower($user->getUsername())])) { + // @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0 + $userIdentifier = strtolower(method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername()); + if (isset($this->users[$userIdentifier])) { throw new \LogicException('Another user with the same username already exists.'); } - $this->users[strtolower($user->getUsername())] = $user; + $this->users[$userIdentifier] = $user; } /** @@ -63,9 +65,17 @@ public function createUser(UserInterface $user) */ public function loadUserByUsername(string $username) { - $user = $this->getUser($username); + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, use loadUserByIdentifier() instead.', __METHOD__); - return new InMemoryUser($user->getUsername(), $user->getPassword(), $user->getRoles(), $user->isEnabled()); + return $this->loadUserByIdentifier($username); + } + + public function loadUserByIdentifier(string $identifier): UserInterface + { + $user = $this->getUser($identifier); + + // @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0 + return new InMemoryUser(method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername(), $user->getPassword(), $user->getRoles(), $user->isEnabled()); } /** @@ -77,7 +87,9 @@ public function refreshUser(UserInterface $user) throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_debug_type($user))); } - $storedUser = $this->getUser($user->getUsername()); + // @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0 + $storedUser = $this->getUser(method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername()); + $userIdentifier = method_exists($storedUser, 'getUserIdentifier') ? $storedUser->getUserIdentifier() : $storedUser->getUsername(); // @deprecated since Symfony 5.3 if (User::class === \get_class($user)) { @@ -91,10 +103,10 @@ public function refreshUser(UserInterface $user) $accountNonLocked = $storedUser->isAccountNonLocked(); } - return new User($storedUser->getUsername(), $storedUser->getPassword(), $storedUser->getRoles(), $storedUser->isEnabled(), $accountNonExpired, $credentialsNonExpired, $accountNonLocked); + return new User($userIdentifier, $storedUser->getPassword(), $storedUser->getRoles(), $storedUser->isEnabled(), $accountNonExpired, $credentialsNonExpired, $accountNonLocked); } - return new InMemoryUser($storedUser->getUsername(), $storedUser->getPassword(), $storedUser->getRoles(), $storedUser->isEnabled()); + return new InMemoryUser($userIdentifier, $storedUser->getPassword(), $storedUser->getRoles(), $storedUser->isEnabled()); } /** @@ -113,13 +125,13 @@ public function supportsClass(string $class) /** * Returns the user by given username. * - * @throws UsernameNotFoundException if user whose given username does not exist + * @throws UserNotFoundException if user whose given username does not exist */ private function getUser(string $username)/*: InMemoryUser */ { if (!isset($this->users[strtolower($username)])) { - $ex = new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username)); - $ex->setUsername($username); + $ex = new UserNotFoundException(sprintf('Username "%s" does not exist.', $username)); + $ex->setUserIdentifier($username); throw $ex; } diff --git a/User/MissingUserProvider.php b/User/MissingUserProvider.php index 605aad6d..02df0516 100644 --- a/User/MissingUserProvider.php +++ b/User/MissingUserProvider.php @@ -37,6 +37,11 @@ public function loadUserByUsername(string $username): UserInterface throw new \BadMethodCallException(); } + public function loadUserByIdentifier(string $identifier): UserInterface + { + throw new \BadMethodCallException(); + } + /** * {@inheritdoc} */ diff --git a/User/User.php b/User/User.php index 045f03bf..d583e5a8 100644 --- a/User/User.php +++ b/User/User.php @@ -53,7 +53,7 @@ public function __construct(?string $username, ?string $password, array $roles = public function __toString(): string { - return $this->getUsername(); + return $this->getUserIdentifier(); } /** @@ -84,6 +84,16 @@ public function getSalt(): ?string * {@inheritdoc} */ public function getUsername(): string + { + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, use getUserIdentifier() instead.', __METHOD__); + + return $this->username; + } + + /** + * Returns the identifier for this user (e.g. its username or e-mailaddress). + */ + public function getUserIdentifier(): string { return $this->username; } @@ -184,7 +194,7 @@ public function isEqualTo(UserInterface $user): bool return false; } - if ($this->getUsername() !== $user->getUsername()) { + if ($this->getUserIdentifier() !== $user->getUserIdentifier()) { return false; } diff --git a/User/UserInterface.php b/User/UserInterface.php index 47661de0..6448ab51 100644 --- a/User/UserInterface.php +++ b/User/UserInterface.php @@ -26,6 +26,8 @@ * * @see UserProviderInterface * + * @method string getUserIdentifier() returns the identifier for this user (e.g. its username or e-mailaddress) + * * @author Fabien Potencier */ interface UserInterface @@ -69,13 +71,6 @@ public function getPassword(); */ public function getSalt(); - /** - * Returns the username used to authenticate the user. - * - * @return string The username - */ - public function getUsername(); - /** * Removes sensitive data from the user. * diff --git a/User/UserProviderInterface.php b/User/UserProviderInterface.php index 708a97f4..5ab67836 100644 --- a/User/UserProviderInterface.php +++ b/User/UserProviderInterface.php @@ -12,16 +12,16 @@ namespace Symfony\Component\Security\Core\User; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; /** * Represents a class that loads UserInterface objects from some source for the authentication system. * - * In a typical authentication configuration, a username (i.e. some unique - * user identifier) credential enters the system (via form login, or any - * method). The user provider that is configured with that authentication - * method is asked to load the UserInterface object for the given username - * (via loadUserByUsername) so that the rest of the process can continue. + * In a typical authentication configuration, a user identifier (e.g. a + * username or e-mailaddress) credential enters the system (via form login, or + * any method). The user provider that is configured with that authentication + * method is asked to load the UserInterface object for the given identifier (via + * loadUserByIdentifier) so that the rest of the process can continue. * * Internally, a user provider can load users from any source (databases, * configuration, web service). This is totally independent of how the authentication @@ -29,22 +29,13 @@ * * @see UserInterface * + * @method UserInterface loadUserByIdentifier(string $identifier) loads the user for the given user identifier (e.g. username or email). + * This method must throw UserNotFoundException if the user is not found. + * * @author Fabien Potencier */ interface UserProviderInterface { - /** - * Loads the user for the given username. - * - * This method must throw UsernameNotFoundException if the user is not - * found. - * - * @return UserInterface - * - * @throws UsernameNotFoundException if the user is not found - */ - public function loadUserByUsername(string $username); - /** * Refreshes the user. * @@ -55,8 +46,8 @@ public function loadUserByUsername(string $username); * * @return UserInterface * - * @throws UnsupportedUserException if the user is not supported - * @throws UsernameNotFoundException if the user is not found + * @throws UnsupportedUserException if the user is not supported + * @throws UserNotFoundException if the user is not found */ public function refreshUser(UserInterface $user); From d857e327bec117aa75171836a57aa8f9445b3725 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 17 Jan 2021 20:20:33 +0100 Subject: [PATCH 15/35] [Security] Rework the remember me system --- .../Exception/ExpiredSignatureException.php | 21 ++++ .../Exception/InvalidSignatureException.php | 21 ++++ Signature/ExpiredSignatureStorage.php | 55 +++++++++++ Signature/SignatureHasher.php | 99 +++++++++++++++++++ .../Signature/ExpiredSignatureStorageTest.php | 29 ++++++ 5 files changed, 225 insertions(+) create mode 100644 Signature/Exception/ExpiredSignatureException.php create mode 100644 Signature/Exception/InvalidSignatureException.php create mode 100644 Signature/ExpiredSignatureStorage.php create mode 100644 Signature/SignatureHasher.php create mode 100644 Tests/Signature/ExpiredSignatureStorageTest.php diff --git a/Signature/Exception/ExpiredSignatureException.php b/Signature/Exception/ExpiredSignatureException.php new file mode 100644 index 00000000..8986c62f --- /dev/null +++ b/Signature/Exception/ExpiredSignatureException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Signature\Exception; + +use Symfony\Component\Security\Core\Exception\RuntimeException; + +/** + * @author Wouter de Jong + */ +class ExpiredSignatureException extends RuntimeException +{ +} diff --git a/Signature/Exception/InvalidSignatureException.php b/Signature/Exception/InvalidSignatureException.php new file mode 100644 index 00000000..72102fe8 --- /dev/null +++ b/Signature/Exception/InvalidSignatureException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Signature\Exception; + +use Symfony\Component\Security\Core\Exception\RuntimeException; + +/** + * @author Wouter de Jong + */ +class InvalidSignatureException extends RuntimeException +{ +} diff --git a/Signature/ExpiredSignatureStorage.php b/Signature/ExpiredSignatureStorage.php new file mode 100644 index 00000000..e5b9f900 --- /dev/null +++ b/Signature/ExpiredSignatureStorage.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Signature; + +use Psr\Cache\CacheItemPoolInterface; + +/** + * @author Ryan Weaver + * + * @experimental in 5.2 + * + * @final + */ +final class ExpiredSignatureStorage +{ + private $cache; + private $lifetime; + + public function __construct(CacheItemPoolInterface $cache, int $lifetime) + { + $this->cache = $cache; + $this->lifetime = $lifetime; + } + + public function countUsages(string $hash): int + { + $key = rawurlencode($hash); + if (!$this->cache->hasItem($key)) { + return 0; + } + + return $this->cache->getItem($key)->get(); + } + + public function incrementUsages(string $hash): void + { + $item = $this->cache->getItem(rawurlencode($hash)); + + if (!$item->isHit()) { + $item->expiresAfter($this->lifetime); + } + + $item->set($this->countUsages($hash) + 1); + $this->cache->save($item); + } +} diff --git a/Signature/SignatureHasher.php b/Signature/SignatureHasher.php new file mode 100644 index 00000000..ad402832 --- /dev/null +++ b/Signature/SignatureHasher.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Signature; + +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\Signature\Exception\ExpiredSignatureException; +use Symfony\Component\Security\Core\Signature\Exception\InvalidSignatureException; +use Symfony\Component\Security\Core\Signature\ExpiredSignatureStorage; + +/** + * Creates and validates secure hashes used in login links and remember-me cookies. + * + * @author Wouter de Jong + * @author Ryan Weaver + */ +class SignatureHasher +{ + private $propertyAccessor; + private $signatureProperties; + private $secret; + private $expiredSignaturesStorage; + private $maxUses; + + /** + * @param array $signatureProperties properties of the User; the hash is invalidated if these properties change + * @param ExpiredSignatureStorage|null $expiredSignaturesStorage if provided, secures a sequence of hashes that are expired + * @param int|null $maxUses used together with $expiredSignatureStorage to allow a maximum usage of a hash + */ + public function __construct(PropertyAccessorInterface $propertyAccessor, array $signatureProperties, string $secret, ?ExpiredSignatureStorage $expiredSignaturesStorage = null, ?int $maxUses = null) + { + $this->propertyAccessor = $propertyAccessor; + $this->signatureProperties = $signatureProperties; + $this->secret = $secret; + $this->expiredSignaturesStorage = $expiredSignaturesStorage; + $this->maxUses = $maxUses; + } + + /** + * Verifies the hash using the provided user and expire time. + * + * @param int $expires the expiry time as a unix timestamp + * @param string $hash the plaintext hash provided by the request + * + * @throws InvalidSignatureException If the signature does not match the provided parameters + * @throws ExpiredSignatureException If the signature is no longer valid + */ + public function verifySignatureHash(UserInterface $user, int $expires, string $hash): void + { + if (!hash_equals($hash, $this->computeSignatureHash($user, $expires))) { + throw new InvalidSignatureException('Invalid or expired signature.'); + } + + if ($expires < time()) { + throw new ExpiredSignatureException('Signature has expired.'); + } + + if ($this->expiredSignaturesStorage && $this->maxUses) { + if ($this->expiredSignaturesStorage->countUsages($hash) >= $this->maxUses) { + throw new ExpiredSignatureException(sprintf('Signature can only be used "%d" times.', $this->maxUses)); + } + + $this->expiredSignaturesStorage->incrementUsages($hash); + } + } + + /** + * Computes the secure hash for the provided user and expire time. + * + * @param int $expires the expiry time as a unix timestamp + */ + public function computeSignatureHash(UserInterface $user, int $expires): string + { + $signatureFields = [base64_encode(method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername()), $expires]; + + foreach ($this->signatureProperties as $property) { + $value = $this->propertyAccessor->getValue($user, $property) ?? ''; + if ($value instanceof \DateTimeInterface) { + $value = $value->format('c'); + } + + if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { + throw new \InvalidArgumentException(sprintf('The property path "%s" on the user object "%s" must return a value that can be cast to a string, but "%s" was returned.', $property, \get_class($user), get_debug_type($value))); + } + $signatureFields[] = base64_encode($value); + } + + return base64_encode(hash_hmac('sha256', implode(':', $signatureFields), $this->secret)); + } +} diff --git a/Tests/Signature/ExpiredSignatureStorageTest.php b/Tests/Signature/ExpiredSignatureStorageTest.php new file mode 100644 index 00000000..7293d873 --- /dev/null +++ b/Tests/Signature/ExpiredSignatureStorageTest.php @@ -0,0 +1,29 @@ + + * + * 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\Signature; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Security\Core\Signature\ExpiredSignatureStorage; + +class ExpiredSignatureStorageTest extends TestCase +{ + public function testUsage() + { + $cache = new ArrayAdapter(); + $storage = new ExpiredSignatureStorage($cache, 600); + + $this->assertSame(0, $storage->countUsages('hash+more')); + $storage->incrementUsages('hash+more'); + $this->assertSame(1, $storage->countUsages('hash+more')); + } +} From 07de03809155838ecb8deb3aa842bb224d9e79fa Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Mon, 12 Apr 2021 18:32:44 +0200 Subject: [PATCH 16/35] [Security] Fix UsageTrackingTokenStorage outside the request cycle --- .../Storage/UsageTrackingTokenStorage.php | 24 +++++++++++-- .../Storage/UsageTrackingTokenStorageTest.php | 36 +++++++++++++++---- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/Authentication/Token/Storage/UsageTrackingTokenStorage.php b/Authentication/Token/Storage/UsageTrackingTokenStorage.php index 0b8d9c32..4b2cac74 100644 --- a/Authentication/Token/Storage/UsageTrackingTokenStorage.php +++ b/Authentication/Token/Storage/UsageTrackingTokenStorage.php @@ -39,7 +39,7 @@ public function __construct(TokenStorageInterface $storage, ContainerInterface $ */ public function getToken(): ?TokenInterface { - if ($this->enableUsageTracking) { + if ($this->shouldTrackUsage()) { // increments the internal session usage index $this->getSession()->getMetadataBag(); } @@ -54,7 +54,7 @@ public function setToken(TokenInterface $token = null): void { $this->storage->setToken($token); - if ($token && $this->enableUsageTracking) { + if ($token && $this->shouldTrackUsage()) { // increments the internal session usage index $this->getSession()->getMetadataBag(); } @@ -88,4 +88,24 @@ private function getSession(): SessionInterface return $this->container->get('request_stack')->getSession(); } + + private function shouldTrackUsage(): bool + { + if (!$this->enableUsageTracking) { + return false; + } + + // BC for symfony/security-bundle < 5.3 + if ($this->container->has('session')) { + return true; + } + + if (!$this->container->get('request_stack')->getMainRequest()) { + trigger_deprecation('symfony/security-core', '5.3', 'Using "%s" (service ID: "security.token_storage") outside the request-response cycle is deprecated, use the "%s" class (service ID: "security.untracked_token_storage") instead or disable usage tracking using "disableUsageTracking()".', __CLASS__, TokenStorage::class); + + return false; + } + + return true; + } } diff --git a/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php b/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php index 38806efa..0d074bd4 100644 --- a/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php +++ b/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php @@ -13,31 +13,37 @@ use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\Security\Core\Authentication\Token\NullToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Contracts\Service\ServiceLocatorTrait; class UsageTrackingTokenStorageTest extends TestCase { + use ExpectDeprecationTrait; + public function testGetSetToken() { $sessionAccess = 0; $sessionLocator = new class(['request_stack' => function () use (&$sessionAccess) { - ++$sessionAccess; - $session = $this->createMock(SessionInterface::class); - $session->expects($this->once()) - ->method('getMetadataBag'); $request = new Request(); $request->setSession($session); - $requestStack = new RequestStack(); + $requestStack = $this->getMockBuilder(RequestStack::class)->setMethods(['getSession'])->getMock(); $requestStack->push($request); + $requestStack->expects($this->any())->method('getSession')->willReturnCallback(function () use ($session, &$sessionAccess) { + ++$sessionAccess; + + $session->expects($this->once()) + ->method('getMetadataBag'); + + return $session; + }); return $requestStack; }]) implements ContainerInterface { @@ -62,4 +68,22 @@ public function testGetSetToken() $this->assertSame($token, $trackingStorage->getToken()); $this->assertSame(1, $sessionAccess); } + + /** + * @group legacy + */ + public function testWithoutMainRequest() + { + $locator = new class(['request_stack' => function () { + return new RequestStack(); + }]) implements ContainerInterface { + use ServiceLocatorTrait; + }; + $tokenStorage = new TokenStorage(); + $trackingStorage = new UsageTrackingTokenStorage($tokenStorage, $locator); + $trackingStorage->enableUsageTracking(); + + $this->expectDeprecation('Since symfony/security-core 5.3: Using "%s" (service ID: "security.token_storage") outside the request-response cycle is deprecated, use the "%s" class (service ID: "security.untracked_token_storage") instead or disable usage tracking using "disableUsageTracking()".'); + $trackingStorage->getToken(); + } } From a112198881c5220b6a0c3f60f5a999908d59353c Mon Sep 17 00:00:00 2001 From: Nyholm Date: Tue, 13 Apr 2021 09:42:19 +0200 Subject: [PATCH 17/35] [Security] Stop using a shared changelog for our security packages --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..22652b08 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.3 +--- + +The CHANGELOG for version 5.3 and earlier can be found at https://github.com/symfony/symfony/blob/5.3/src/Symfony/Component/Security/CHANGELOG.md From 3b2d28a390e8d468b5cc3f897be06f9ca6696d0a Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Wed, 14 Apr 2021 10:40:21 +0200 Subject: [PATCH 18/35] =?UTF-8?q?Remove=20experimental=20flag=20from=20the?= =?UTF-8?q?=20authenticator=20system=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Signature/ExpiredSignatureStorage.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/Signature/ExpiredSignatureStorage.php b/Signature/ExpiredSignatureStorage.php index e5b9f900..5421c77e 100644 --- a/Signature/ExpiredSignatureStorage.php +++ b/Signature/ExpiredSignatureStorage.php @@ -16,8 +16,6 @@ /** * @author Ryan Weaver * - * @experimental in 5.2 - * * @final */ final class ExpiredSignatureStorage From 9e5de7f04d8184f1bb4f1c1f7d807625a97bc56b Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Sat, 1 May 2021 20:09:29 +0200 Subject: [PATCH 19/35] [PasswordHasher] Improved BC layer --- Encoder/EncoderFactory.php | 10 ++++- Encoder/LegacyPasswordHasherEncoder.php | 52 +++++++++++++++++++++ Encoder/PasswordHasherAdapter.php | 46 +++++++++++++++++++ Encoder/PasswordHasherEncoder.php | 60 +++++++++++++++++++++++++ Tests/Encoder/EncoderFactoryTest.php | 32 +++++++++++-- 5 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 Encoder/LegacyPasswordHasherEncoder.php create mode 100644 Encoder/PasswordHasherAdapter.php create mode 100644 Encoder/PasswordHasherEncoder.php diff --git a/Encoder/EncoderFactory.php b/Encoder/EncoderFactory.php index d1855aa1..526c461e 100644 --- a/Encoder/EncoderFactory.php +++ b/Encoder/EncoderFactory.php @@ -13,6 +13,8 @@ use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface; use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory; +use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface; +use Symfony\Component\PasswordHasher\PasswordHasherInterface; use Symfony\Component\Security\Core\Exception\LogicException; trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', EncoderFactory::class, PasswordHasherFactory::class); @@ -60,7 +62,13 @@ public function getEncoder($user) } if (!$this->encoders[$encoderKey] instanceof PasswordEncoderInterface) { - $this->encoders[$encoderKey] = $this->createEncoder($this->encoders[$encoderKey]); + if ($this->encoders[$encoderKey] instanceof LegacyPasswordHasherInterface) { + $this->encoders[$encoderKey] = new LegacyPasswordHasherEncoder($this->encoders[$encoderKey]); + } elseif ($this->encoders[$encoderKey] instanceof PasswordHasherInterface) { + $this->encoders[$encoderKey] = new PasswordHasherEncoder($this->encoders[$encoderKey]); + } else { + $this->encoders[$encoderKey] = $this->createEncoder($this->encoders[$encoderKey]); + } } return $this->encoders[$encoderKey]; diff --git a/Encoder/LegacyPasswordHasherEncoder.php b/Encoder/LegacyPasswordHasherEncoder.php new file mode 100644 index 00000000..7e57ff23 --- /dev/null +++ b/Encoder/LegacyPasswordHasherEncoder.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Encoder; + +use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException; +use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; + +/** + * Forward compatibility for new new PasswordHasher component. + * + * @author Alexander M. Turek + * + * @internal To be removed in Symfony 6 + */ +final class LegacyPasswordHasherEncoder implements PasswordEncoderInterface +{ + private $passwordHasher; + + public function __construct(LegacyPasswordHasherInterface $passwordHasher) + { + $this->passwordHasher = $passwordHasher; + } + + public function encodePassword(string $raw, ?string $salt): string + { + try { + return $this->passwordHasher->hash($raw, $salt); + } catch (InvalidPasswordException $e) { + throw new BadCredentialsException($e->getMessage(), $e->getCode(), $e); + } + } + + public function isPasswordValid(string $encoded, string $raw, ?string $salt): bool + { + return $this->passwordHasher->verify($encoded, $raw, $salt); + } + + public function needsRehash(string $encoded): bool + { + return $this->passwordHasher->needsRehash($encoded); + } +} diff --git a/Encoder/PasswordHasherAdapter.php b/Encoder/PasswordHasherAdapter.php new file mode 100644 index 00000000..4a4b9c0b --- /dev/null +++ b/Encoder/PasswordHasherAdapter.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Encoder; + +use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface; + +/** + * Forward compatibility for new new PasswordHasher component. + * + * @author Alexander M. Turek + * + * @internal To be removed in Symfony 6 + */ +final class PasswordHasherAdapter implements LegacyPasswordHasherInterface +{ + private $passwordEncoder; + + public function __construct(PasswordEncoderInterface $passwordEncoder) + { + $this->passwordEncoder = $passwordEncoder; + } + + public function hash(string $plainPassword, ?string $salt = null): string + { + return $this->passwordEncoder->encodePassword($plainPassword, $salt); + } + + public function verify(string $hashedPassword, string $plainPassword, ?string $salt = null): bool + { + return $this->passwordEncoder->isPasswordValid($hashedPassword, $plainPassword, $salt); + } + + public function needsRehash(string $hashedPassword): bool + { + return $this->passwordEncoder->needsRehash($hashedPassword); + } +} diff --git a/Encoder/PasswordHasherEncoder.php b/Encoder/PasswordHasherEncoder.php new file mode 100644 index 00000000..d37875dc --- /dev/null +++ b/Encoder/PasswordHasherEncoder.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Encoder; + +use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException; +use Symfony\Component\PasswordHasher\PasswordHasherInterface; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; + +/** + * Forward compatibility for new new PasswordHasher component. + * + * @author Alexander M. Turek + * + * @internal To be removed in Symfony 6 + */ +final class PasswordHasherEncoder implements PasswordEncoderInterface, SelfSaltingEncoderInterface +{ + private $passwordHasher; + + public function __construct(PasswordHasherInterface $passwordHasher) + { + $this->passwordHasher = $passwordHasher; + } + + public function encodePassword(string $raw, ?string $salt): string + { + if (null !== $salt) { + throw new \InvalidArgumentException('This password hasher does not support passing a salt.'); + } + + try { + return $this->passwordHasher->hash($raw); + } catch (InvalidPasswordException $e) { + throw new BadCredentialsException($e->getMessage(), $e->getCode(), $e); + } + } + + public function isPasswordValid(string $encoded, string $raw, ?string $salt): bool + { + if (null !== $salt) { + throw new \InvalidArgumentException('This password hasher does not support passing a salt.'); + } + + return $this->passwordHasher->verify($encoded, $raw); + } + + public function needsRehash(string $encoded): bool + { + return $this->passwordHasher->needsRehash($encoded); + } +} diff --git a/Tests/Encoder/EncoderFactoryTest.php b/Tests/Encoder/EncoderFactoryTest.php index 3744e05b..7b05c9be 100644 --- a/Tests/Encoder/EncoderFactoryTest.php +++ b/Tests/Encoder/EncoderFactoryTest.php @@ -12,17 +12,20 @@ namespace Symfony\Component\Security\Core\Tests\Encoder; use PHPUnit\Framework\TestCase; +use Symfony\Component\PasswordHasher\Hasher\MessageDigestPasswordHasher; +use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory; +use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher; use Symfony\Component\Security\Core\Encoder\EncoderAwareInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactory; use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder; use Symfony\Component\Security\Core\Encoder\MigratingPasswordEncoder; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; +use Symfony\Component\Security\Core\Encoder\SelfSaltingEncoderInterface; use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface; -use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory; -use Symfony\Component\PasswordHasher\Hasher\MessageDigestPasswordHasher; /** * @group legacy @@ -193,6 +196,28 @@ public function testHasherAwareCompat() $expectedEncoder = new MessageDigestPasswordHasher('sha1'); $this->assertEquals($expectedEncoder->hash('foo', ''), $encoder->hash('foo', '')); } + + public function testLegacyPasswordHasher() + { + $factory = new EncoderFactory([ + SomeUser::class => new PlaintextPasswordHasher(), + ]); + + $encoder = $factory->getEncoder(new SomeUser()); + self::assertNotInstanceOf(SelfSaltingEncoderInterface::class, $encoder); + self::assertSame('foo{bar}', $encoder->encodePassword('foo', 'bar')); + } + + public function testPasswordHasher() + { + $factory = new EncoderFactory([ + SomeUser::class => new NativePasswordHasher(), + ]); + + $encoder = $factory->getEncoder(new SomeUser()); + self::assertInstanceOf(SelfSaltingEncoderInterface::class, $encoder); + self::assertTrue($encoder->isPasswordValid($encoder->encodePassword('foo', null), 'foo', null)); + } } class SomeUser implements UserInterface @@ -236,7 +261,6 @@ public function getEncoderName(): ?string } } - class HasherAwareUser extends SomeUser implements PasswordHasherAwareInterface { public $hasherName = 'encoder_name'; From 1ab9460c2e19d71f6d219247103ac4db36da4cd7 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Mon, 3 May 2021 17:52:09 +0200 Subject: [PATCH 20/35] Make Serializable implementation internal and final --- Authentication/Token/NullToken.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Authentication/Token/NullToken.php b/Authentication/Token/NullToken.php index 5c8a1c24..4393f0bd 100644 --- a/Authentication/Token/NullToken.php +++ b/Authentication/Token/NullToken.php @@ -103,6 +103,9 @@ public function __unserialize(array $data): void /** * @return string + * + * @internal in 5.3 + * @final in 5.3 */ public function serialize() { @@ -111,6 +114,9 @@ public function serialize() /** * @return void + * + * @internal in 5.3 + * @final in 5.3 */ public function unserialize($serialized) { From 3ab721be77e91900c5ae1c773feaad63fbae35d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rokas=20Mikalk=C4=97nas?= Date: Mon, 3 May 2021 20:17:38 +0300 Subject: [PATCH 21/35] Missing security lt translations added --- Resources/translations/security.lt.xlf | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Resources/translations/security.lt.xlf b/Resources/translations/security.lt.xlf index 37487b79..b4daa08b 100644 --- a/Resources/translations/security.lt.xlf +++ b/Resources/translations/security.lt.xlf @@ -70,6 +70,14 @@ Invalid or expired login link. Netinkama arba pasibaigusio galiojimo laiko prisijungimo nuoroda. + + Too many failed login attempts, please try again in %minutes% minute. + Per daug nepavykusių prisijungimo bandymų, pabandykite dar kartą po %minutes% minutės. + + + Too many failed login attempts, please try again in %minutes% minutes. + Per daug nepavykusių prisijungimo bandymų, pabandykite dar kartą po %minutes% minutės.|Per daug nepavykusių prisijungimo bandymų, pabandykite dar kartą po %minutes% minučių.|Per daug nepavykusių prisijungimo bandymų, pabandykite dar kartą po %minutes% minučių. + From 5ff7c951c8af82bb86490b3bb45df8eeb4cc9acd Mon Sep 17 00:00:00 2001 From: Andrii Bodnar Date: Wed, 12 May 2021 15:53:46 +0300 Subject: [PATCH 22/35] [Security] Added Ukrainian translations --- Resources/translations/security.uk.xlf | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Resources/translations/security.uk.xlf b/Resources/translations/security.uk.xlf index dc90c91f..6d5cff42 100644 --- a/Resources/translations/security.uk.xlf +++ b/Resources/translations/security.uk.xlf @@ -70,6 +70,14 @@ Invalid or expired login link. Посилання для входу недійсне, або термін його дії закінчився. + + Too many failed login attempts, please try again in %minutes% minute. + Забагато невдалих спроб входу. Будь ласка, спробуйте знову через %minutes% хвилину. + + + Too many failed login attempts, please try again in %minutes% minutes. + Забагато невдалих спроб входу. Будь ласка, спробуйте знову через %minutes% хв. + From f3e8eb97c11088125745d9454b942cbc47e3756b Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Fri, 14 May 2021 22:24:36 +0200 Subject: [PATCH 23/35] Added and improved Bulgarian translations --- Resources/translations/security.bg.xlf | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Resources/translations/security.bg.xlf b/Resources/translations/security.bg.xlf index 318f7d49..ccf24256 100644 --- a/Resources/translations/security.bg.xlf +++ b/Resources/translations/security.bg.xlf @@ -20,7 +20,7 @@ Cookie has already been used by someone else. - Това cookie вече се ползва от някой друг. + Тази бисквитка вече се ползва от някой друг. Not privileged to request the resource. @@ -36,11 +36,11 @@ No session available, it either timed out or cookies are not enabled. - Сесията не е достъпна, или времето за достъп е изтекло, или кукитата не са разрешени. + Сесията не е достъпна, или времето за достъп е изтекло, или бисквитките не са разрешени. No token could be found. - Токена не е открит. + Токенът не е открит. Username could not be found. @@ -48,7 +48,7 @@ Account has expired. - Акаунта е изтекъл. + Акаунтът е изтекъл. Credentials have expired. @@ -56,20 +56,28 @@ Account is disabled. - Акаунта е деактивиран. + Акаунтът е деактивиран. Account is locked. - Акаунта е заключен. + Акаунтът е заключен. Too many failed login attempts, please try again later. - Твърде много грешни опити за вход, моля опитайте по-късно. + Твърде много неуспешни опити за вход, моля опитайте по-късно. Invalid or expired login link. Невалиден или изтекъл линк за вход. + + Too many failed login attempts, please try again in %minutes% minute. + Прекалено много неуспешни опити за вход, моля опитайте отново след %minutes% минута. + + + Too many failed login attempts, please try again in %minutes% minutes. + Прекалено много неуспешни опити за вход, моля опитайте отново след %minutes% минути. + From 6ba52f83d5d5a6cd16e7a7db8aec7c71cafaf91c Mon Sep 17 00:00:00 2001 From: Warxcell Date: Sun, 16 May 2021 11:22:39 +0300 Subject: [PATCH 24/35] [Security] Keep Bulgarian wording consistent across all texts. --- Resources/translations/security.bg.xlf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Resources/translations/security.bg.xlf b/Resources/translations/security.bg.xlf index ccf24256..1d45b28c 100644 --- a/Resources/translations/security.bg.xlf +++ b/Resources/translations/security.bg.xlf @@ -72,11 +72,11 @@ Too many failed login attempts, please try again in %minutes% minute. - Прекалено много неуспешни опити за вход, моля опитайте отново след %minutes% минута. + Твърде много неуспешни опити за вход, моля опитайте отново след %minutes% минута. Too many failed login attempts, please try again in %minutes% minutes. - Прекалено много неуспешни опити за вход, моля опитайте отново след %minutes% минути. + Твърде много неуспешни опити за вход, моля опитайте отново след %minutes% минути. From 0556d576adce9a401a021119c555786e1b53ce79 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Sat, 15 May 2021 19:17:06 +0200 Subject: [PATCH 25/35] Fixed deprecation warnings about passing null as parameter --- .../Provider/LdapBindAuthenticationProviderTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php b/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php index 0605df44..2bc2d017 100644 --- a/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php @@ -70,6 +70,7 @@ public function testBindFailureShouldThrowAnException() ->method('bind') ->willThrowException(new ConnectionException()) ; + $ldap->method('escape')->willReturnArgument(0); $userChecker = $this->createMock(UserCheckerInterface::class); $provider = new LdapBindAuthenticationProvider($userProvider, $userChecker, 'key', $ldap); @@ -207,6 +208,7 @@ public function testEmptyQueryResultShouldThrowAnException() ->method('query') ->willReturn($query) ; + $ldap->method('escape')->willReturnArgument(0); $userChecker = $this->createMock(UserCheckerInterface::class); $provider = new LdapBindAuthenticationProvider($userProvider, $userChecker, 'key', $ldap, '{username}', true, 'elsa', 'test1234A$'); From b3f6b5526b5b9749de50c74397747f6fb05601b4 Mon Sep 17 00:00:00 2001 From: fd6130 Date: Sun, 16 May 2021 22:49:50 +0800 Subject: [PATCH 26/35] add chinese translation --- Resources/translations/security.zh_CN.xlf | 8 ++++++++ Resources/translations/security.zh_TW.xlf | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/Resources/translations/security.zh_CN.xlf b/Resources/translations/security.zh_CN.xlf index ce9d6fd2..6c4934ed 100644 --- a/Resources/translations/security.zh_CN.xlf +++ b/Resources/translations/security.zh_CN.xlf @@ -70,6 +70,14 @@ Invalid or expired login link. 失效或过期的登入链接。 + + Too many failed login attempts, please try again in %minutes% minute. + 登入失败的次数过多,请在%minutes%分钟后再试。 + + + Too many failed login attempts, please try again in %minutes% minutes. + 登入失败的次数过多,请在%minutes%分钟后再试。 + diff --git a/Resources/translations/security.zh_TW.xlf b/Resources/translations/security.zh_TW.xlf index 86310473..fd305879 100644 --- a/Resources/translations/security.zh_TW.xlf +++ b/Resources/translations/security.zh_TW.xlf @@ -70,6 +70,14 @@ Invalid or expired login link. 失效或過期的登入鏈接。 + + Too many failed login attempts, please try again in %minutes% minute. + 登錄失敗的次數過多,請在%minutes%分鐘後再試。 + + + Too many failed login attempts, please try again in %minutes% minutes. + 登錄失敗的次數過多,請在%minutes%分鐘後再試。 + From 5b8e39c5ffadad404ef0af6270d6df08f19885f6 Mon Sep 17 00:00:00 2001 From: Aleksandar Jakovljevic Date: Tue, 18 May 2021 10:13:34 +0200 Subject: [PATCH 27/35] [Security] Added missing translations for Serbian (sr_Latn) #41066 --- Resources/translations/security.sr_Latn.xlf | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Resources/translations/security.sr_Latn.xlf b/Resources/translations/security.sr_Latn.xlf index 219281d6..f3de5de5 100644 --- a/Resources/translations/security.sr_Latn.xlf +++ b/Resources/translations/security.sr_Latn.xlf @@ -70,6 +70,14 @@ Invalid or expired login link. Link za prijavljivanje je istekao ili je neispravan. + + Too many failed login attempts, please try again in %minutes% minute. + Previše neuspešnih pokušaja prijavljivanja, molim pokušajte ponovo za %minutes% minut. + + + Too many failed login attempts, please try again in %minutes% minutes. + Previše neuspešnih pokušaja prijavljivanja, molim pokušajte ponovo za %minutes% minuta. + From 754d4dd5bf6eb5a57caa092adc33c76b26cda123 Mon Sep 17 00:00:00 2001 From: ajakov Date: Tue, 18 May 2021 10:51:58 +0200 Subject: [PATCH 28/35] minor #41065 [Security] Added missing translations for Serbian (sr_Cyrl) --- Resources/translations/security.sr_Cyrl.xlf | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Resources/translations/security.sr_Cyrl.xlf b/Resources/translations/security.sr_Cyrl.xlf index 92ba9004..97549bd7 100644 --- a/Resources/translations/security.sr_Cyrl.xlf +++ b/Resources/translations/security.sr_Cyrl.xlf @@ -70,6 +70,14 @@ Invalid or expired login link. Линк за пријављивање је истекао или је неисправан. + + Too many failed login attempts, please try again in %minutes% minute. + Превише неуспешних покушаја пријављивања, молим покушајте поново за %minutes% минут. + + + Too many failed login attempts, please try again in %minutes% minutes. + Превише неуспешних покушаја пријављивања, молим покушајте поново за %minutes% минута. + From b410114e8813f8dc662b51fd9ec0c8347d06c57c Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Sun, 9 May 2021 16:09:05 +0200 Subject: [PATCH 29/35] [Security] Deprecate the old authentication mechanisms --- Authentication/AuthenticationManagerInterface.php | 2 ++ Authentication/AuthenticationProviderManager.php | 4 ++++ Authentication/Provider/AnonymousAuthenticationProvider.php | 4 ++++ Authentication/Provider/AuthenticationProviderInterface.php | 4 ++++ Authentication/Provider/DaoAuthenticationProvider.php | 4 ++++ Authentication/Provider/LdapBindAuthenticationProvider.php | 4 ++++ .../Provider/PreAuthenticatedAuthenticationProvider.php | 4 ++++ Authentication/Provider/RememberMeAuthenticationProvider.php | 5 +++++ Authentication/Provider/UserAuthenticationProvider.php | 4 ++++ Authentication/Token/AbstractToken.php | 2 +- Event/AuthenticationFailureEvent.php | 5 +++++ Tests/Authentication/AuthenticationProviderManagerTest.php | 3 +++ .../Provider/AnonymousAuthenticationProviderTest.php | 3 +++ .../Provider/LdapBindAuthenticationProviderTest.php | 1 + .../Provider/PreAuthenticatedAuthenticationProviderTest.php | 3 +++ .../Provider/RememberMeAuthenticationProviderTest.php | 3 +++ 16 files changed, 54 insertions(+), 1 deletion(-) diff --git a/Authentication/AuthenticationManagerInterface.php b/Authentication/AuthenticationManagerInterface.php index 6237f79a..6776ee78 100644 --- a/Authentication/AuthenticationManagerInterface.php +++ b/Authentication/AuthenticationManagerInterface.php @@ -19,6 +19,8 @@ * which process Token authentication. * * @author Fabien Potencier + * + * @internal since Symfony 5.3 */ interface AuthenticationManagerInterface { diff --git a/Authentication/AuthenticationProviderManager.php b/Authentication/AuthenticationProviderManager.php index ddf09830..92a48dc9 100644 --- a/Authentication/AuthenticationProviderManager.php +++ b/Authentication/AuthenticationProviderManager.php @@ -24,6 +24,8 @@ use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use the new authenticator system instead.', AuthenticationProviderManager::class); + // Help opcache.preload discover always-needed symbols class_exists(AuthenticationEvents::class); class_exists(AuthenticationFailureEvent::class); @@ -35,6 +37,8 @@ class_exists(AuthenticationSuccessEvent::class); * * @author Fabien Potencier * @author Johannes M. Schmitt + * + * @deprecated since Symfony 5.3, use the new authenticator system instead */ class AuthenticationProviderManager implements AuthenticationManagerInterface { diff --git a/Authentication/Provider/AnonymousAuthenticationProvider.php b/Authentication/Provider/AnonymousAuthenticationProvider.php index bbb930d5..53f8cf18 100644 --- a/Authentication/Provider/AnonymousAuthenticationProvider.php +++ b/Authentication/Provider/AnonymousAuthenticationProvider.php @@ -16,10 +16,14 @@ use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use the new authenticator system instead.', AnonymousAuthenticationProvider::class); + /** * AnonymousAuthenticationProvider validates AnonymousToken instances. * * @author Fabien Potencier + * + * @deprecated since Symfony 5.3, use the new authenticator system instead */ class AnonymousAuthenticationProvider implements AuthenticationProviderInterface { diff --git a/Authentication/Provider/AuthenticationProviderInterface.php b/Authentication/Provider/AuthenticationProviderInterface.php index 66387268..e2dee80b 100644 --- a/Authentication/Provider/AuthenticationProviderInterface.php +++ b/Authentication/Provider/AuthenticationProviderInterface.php @@ -14,6 +14,8 @@ use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" interface is deprecated, use the new authenticator system instead.', AuthenticationProviderInterface::class); + /** * AuthenticationProviderInterface is the interface for all authentication * providers. @@ -21,6 +23,8 @@ * Concrete implementations processes specific Token instances. * * @author Fabien Potencier + * + * @deprecated since Symfony 5.3, use the new authenticator system instead */ interface AuthenticationProviderInterface extends AuthenticationManagerInterface { diff --git a/Authentication/Provider/DaoAuthenticationProvider.php b/Authentication/Provider/DaoAuthenticationProvider.php index 4ef55664..d83c1a0c 100644 --- a/Authentication/Provider/DaoAuthenticationProvider.php +++ b/Authentication/Provider/DaoAuthenticationProvider.php @@ -24,11 +24,15 @@ use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use the new authenticator system instead.', DaoAuthenticationProvider::class); + /** * DaoAuthenticationProvider uses a UserProviderInterface to retrieve the user * for a UsernamePasswordToken. * * @author Fabien Potencier + * + * @deprecated since Symfony 5.3, use the new authenticator system instead */ class DaoAuthenticationProvider extends UserAuthenticationProvider { diff --git a/Authentication/Provider/LdapBindAuthenticationProvider.php b/Authentication/Provider/LdapBindAuthenticationProvider.php index e9a3ab02..418523e2 100644 --- a/Authentication/Provider/LdapBindAuthenticationProvider.php +++ b/Authentication/Provider/LdapBindAuthenticationProvider.php @@ -21,6 +21,8 @@ use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use the new authenticator system instead.', LdapBindAuthenticationProvider::class); + /** * LdapBindAuthenticationProvider authenticates a user against an LDAP server. * @@ -28,6 +30,8 @@ * credentials to the ldap. * * @author Charles Sarrazin + * + * @deprecated since Symfony 5.3, use the new authenticator system instead */ class LdapBindAuthenticationProvider extends UserAuthenticationProvider { diff --git a/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php b/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php index 292b8b9f..4f69f33a 100644 --- a/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php +++ b/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php @@ -18,6 +18,8 @@ use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use the new authenticator system instead.', PreAuthenticatedAuthenticationProvider::class); + /** * Processes a pre-authenticated authentication request. * @@ -27,6 +29,8 @@ * UserNotFoundException, for example. * * @author Fabien Potencier + * + * @deprecated since Symfony 5.3, use the new authenticator system instead */ class PreAuthenticatedAuthenticationProvider implements AuthenticationProviderInterface { diff --git a/Authentication/Provider/RememberMeAuthenticationProvider.php b/Authentication/Provider/RememberMeAuthenticationProvider.php index 8ee8109b..2fd52f2d 100644 --- a/Authentication/Provider/RememberMeAuthenticationProvider.php +++ b/Authentication/Provider/RememberMeAuthenticationProvider.php @@ -19,6 +19,11 @@ use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use the new authenticator system instead.', RememberMeAuthenticationProvider::class); + +/** + * @deprecated since Symfony 5.3, use the new authenticator system instead + */ class RememberMeAuthenticationProvider implements AuthenticationProviderInterface { private $userChecker; diff --git a/Authentication/Provider/UserAuthenticationProvider.php b/Authentication/Provider/UserAuthenticationProvider.php index a4811faf..61226a5e 100644 --- a/Authentication/Provider/UserAuthenticationProvider.php +++ b/Authentication/Provider/UserAuthenticationProvider.php @@ -22,10 +22,14 @@ use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use the new authenticator system instead.', UserAuthenticationProvider::class); + /** * UserProviderInterface retrieves users for UsernamePasswordToken tokens. * * @author Fabien Potencier + * + * @deprecated since Symfony 5.3, use the new authenticator system instead */ abstract class UserAuthenticationProvider implements AuthenticationProviderInterface { diff --git a/Authentication/Token/AbstractToken.php b/Authentication/Token/AbstractToken.php index b7934137..a68a27d4 100644 --- a/Authentication/Token/AbstractToken.php +++ b/Authentication/Token/AbstractToken.php @@ -68,7 +68,7 @@ public function getUsername(/* $legacy = true */) public function getUserIdentifier(): string { - // method returns "null" in non-legacy mode if not overriden + // method returns "null" in non-legacy mode if not overridden $username = $this->getUsername(false); if (null !== $username) { trigger_deprecation('symfony/security-core', '5.3', 'Method "%s::getUsername()" is deprecated, override "getUserIdentifier()" instead.', get_debug_type($this)); diff --git a/Event/AuthenticationFailureEvent.php b/Event/AuthenticationFailureEvent.php index e286e24f..4e9562c2 100644 --- a/Event/AuthenticationFailureEvent.php +++ b/Event/AuthenticationFailureEvent.php @@ -13,11 +13,16 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Http\Event\LoginFailureEvent; + +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" with the new authenticator system instead.', AuthenticationFailureEvent::class, LoginFailureEvent::class); /** * This event is dispatched on authentication failure. * * @author Johannes M. Schmitt + * + * @deprecated since Symfony 5.3, use LoginFailureEvent with the new authenticator system instead */ final class AuthenticationFailureEvent extends AuthenticationEvent { diff --git a/Tests/Authentication/AuthenticationProviderManagerTest.php b/Tests/Authentication/AuthenticationProviderManagerTest.php index d41805bf..661ffa45 100644 --- a/Tests/Authentication/AuthenticationProviderManagerTest.php +++ b/Tests/Authentication/AuthenticationProviderManagerTest.php @@ -25,6 +25,9 @@ use Symfony\Component\Security\Core\Exception\ProviderNotFoundException; use Symfony\Component\Security\Core\User\InMemoryUser; +/** + * @group legacy + */ class AuthenticationProviderManagerTest extends TestCase { public function testAuthenticateWithoutProviders() diff --git a/Tests/Authentication/Provider/AnonymousAuthenticationProviderTest.php b/Tests/Authentication/Provider/AnonymousAuthenticationProviderTest.php index 5aa23d98..08127b6c 100644 --- a/Tests/Authentication/Provider/AnonymousAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/AnonymousAuthenticationProviderTest.php @@ -18,6 +18,9 @@ use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; +/** + * @group legacy + */ class AnonymousAuthenticationProviderTest extends TestCase { public function testSupports() diff --git a/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php b/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php index eb9095e2..27dc2acc 100644 --- a/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php @@ -27,6 +27,7 @@ /** * @requires extension ldap + * @group legacy */ class LdapBindAuthenticationProviderTest extends TestCase { diff --git a/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php b/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php index 15c079b8..f7f5fb45 100644 --- a/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php @@ -23,6 +23,9 @@ use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +/** + * @group legacy + */ class PreAuthenticatedAuthenticationProviderTest extends TestCase { public function testSupports() diff --git a/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php b/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php index 41994e7b..9a6a417b 100644 --- a/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php @@ -23,6 +23,9 @@ use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; +/** + * @group legacy + */ class RememberMeAuthenticationProviderTest extends TestCase { public function testSupports() From d296685245ff74ed17a9b252ae6f237b39c2aa1d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 11 May 2021 15:42:06 +0200 Subject: [PATCH 30/35] [Security] [RememberMe] Add support for parallel requests doing remember-me re-authentication --- .../RememberMe/CacheTokenVerifier.php | 68 +++++++++++++++++++ .../RememberMe/TokenVerifierInterface.php | 32 +++++++++ .../RememberMe/CacheTokenVerifierTest.php | 43 ++++++++++++ composer.json | 2 + 4 files changed, 145 insertions(+) create mode 100644 Authentication/RememberMe/CacheTokenVerifier.php create mode 100644 Authentication/RememberMe/TokenVerifierInterface.php create mode 100644 Tests/Authentication/RememberMe/CacheTokenVerifierTest.php diff --git a/Authentication/RememberMe/CacheTokenVerifier.php b/Authentication/RememberMe/CacheTokenVerifier.php new file mode 100644 index 00000000..1f4241e6 --- /dev/null +++ b/Authentication/RememberMe/CacheTokenVerifier.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\RememberMe; + +use Psr\Cache\CacheItemPoolInterface; + +/** + * @author Jordi Boggiano + */ +class CacheTokenVerifier implements TokenVerifierInterface +{ + private $cache; + private $outdatedTokenTtl; + private $cacheKeyPrefix; + + /** + * @param int $outdatedTokenTtl How long the outdated token should still be considered valid. Defaults + * to 60, which matches how often the PersistentRememberMeHandler will at + * most refresh tokens. Increasing to more than that is not recommended, + * but you may use a lower value. + */ + public function __construct(CacheItemPoolInterface $cache, int $outdatedTokenTtl = 60, string $cacheKeyPrefix = 'rememberme-stale-') + { + $this->cache = $cache; + $this->outdatedTokenTtl = $outdatedTokenTtl; + } + + /** + * {@inheritdoc} + */ + public function verifyToken(PersistentTokenInterface $token, string $tokenValue): bool + { + if (hash_equals($token->getTokenValue(), $tokenValue)) { + return true; + } + + if (!$this->cache->hasItem($this->cacheKeyPrefix.$token->getSeries())) { + return false; + } + + $item = $this->cache->getItem($this->cacheKeyPrefix.$token->getSeries()); + $outdatedToken = $item->get(); + + return hash_equals($outdatedToken, $tokenValue); + } + + /** + * {@inheritdoc} + */ + public function updateExistingToken(PersistentTokenInterface $token, string $tokenValue, \DateTimeInterface $lastUsed): void + { + // When a token gets updated, persist the outdated token for $outdatedTokenTtl seconds so we can + // still accept it as valid in verifyToken + $item = $this->cache->getItem($this->cacheKeyPrefix.$token->getSeries()); + $item->set($token->getTokenValue()); + $item->expiresAfter($this->outdatedTokenTtl); + $this->cache->save($item); + } +} diff --git a/Authentication/RememberMe/TokenVerifierInterface.php b/Authentication/RememberMe/TokenVerifierInterface.php new file mode 100644 index 00000000..57278d9e --- /dev/null +++ b/Authentication/RememberMe/TokenVerifierInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\RememberMe; + +/** + * @author Jordi Boggiano + */ +interface TokenVerifierInterface +{ + /** + * Verifies that the given $token is valid. + * + * This lets you override the token check logic to for example accept slightly outdated tokens. + * + * Do not forget to implement token comparisons using hash_equals for a secure implementation. + */ + public function verifyToken(PersistentTokenInterface $token, string $tokenValue): bool; + + /** + * Updates an existing token with a new token value and lastUsed time. + */ + public function updateExistingToken(PersistentTokenInterface $token, string $tokenValue, \DateTimeInterface $lastUsed): void; +} diff --git a/Tests/Authentication/RememberMe/CacheTokenVerifierTest.php b/Tests/Authentication/RememberMe/CacheTokenVerifierTest.php new file mode 100644 index 00000000..709ad283 --- /dev/null +++ b/Tests/Authentication/RememberMe/CacheTokenVerifierTest.php @@ -0,0 +1,43 @@ + + * + * 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\Authentication\RememberMe; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Security\Core\Authentication\RememberMe\CacheTokenVerifier; +use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken; + +class CacheTokenVerifierTest extends TestCase +{ + public function testVerifyCurrentToken() + { + $verifier = new CacheTokenVerifier(new ArrayAdapter()); + $token = new PersistentToken('class', 'user', 'series1', 'value', new \DateTime()); + $this->assertTrue($verifier->verifyToken($token, 'value')); + } + + public function testVerifyFailsInvalidToken() + { + $verifier = new CacheTokenVerifier(new ArrayAdapter()); + $token = new PersistentToken('class', 'user', 'series1', 'value', new \DateTime()); + $this->assertFalse($verifier->verifyToken($token, 'wrong-value')); + } + + public function testVerifyOutdatedToken() + { + $verifier = new CacheTokenVerifier(new ArrayAdapter()); + $outdatedToken = new PersistentToken('class', 'user', 'series1', 'value', new \DateTime()); + $newToken = new PersistentToken('class', 'user', 'series1', 'newvalue', new \DateTime()); + $verifier->updateExistingToken($outdatedToken, 'newvalue', new \DateTime()); + $this->assertTrue($verifier->verifyToken($newToken, 'value')); + } +} diff --git a/composer.json b/composer.json index e53eecfe..d129ffee 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,8 @@ }, "require-dev": { "psr/container": "^1.0|^2.0", + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/cache": "^4.4|^5.0", "symfony/event-dispatcher": "^4.4|^5.0", "symfony/expression-language": "^4.4|^5.0", "symfony/http-foundation": "^5.3", From 6eea784297bd604efc169e1fc6b63c55b25d5bc6 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Thu, 13 May 2021 12:05:25 +0200 Subject: [PATCH 31/35] [Security\Core] Fix user enumeration via response body on invalid credentials --- .../Provider/UserAuthenticationProvider.php | 4 ++-- .../UserAuthenticationProviderTest.php | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Authentication/Provider/UserAuthenticationProvider.php b/Authentication/Provider/UserAuthenticationProvider.php index 9557fa00..e5357603 100644 --- a/Authentication/Provider/UserAuthenticationProvider.php +++ b/Authentication/Provider/UserAuthenticationProvider.php @@ -84,8 +84,8 @@ public function authenticate(TokenInterface $token) $this->userChecker->checkPreAuth($user); $this->checkAuthentication($user, $token); $this->userChecker->checkPostAuth($user); - } catch (AccountStatusException $e) { - if ($this->hideUserNotFoundExceptions) { + } catch (AuthenticationException $e) { + if ($this->hideUserNotFoundExceptions && ($e instanceof AccountStatusException || $e instanceof BadCredentialsException)) { throw new BadCredentialsException('Bad credentials.', 0, $e); } diff --git a/Tests/Authentication/Provider/UserAuthenticationProviderTest.php b/Tests/Authentication/Provider/UserAuthenticationProviderTest.php index c20b6ca2..92f987d1 100644 --- a/Tests/Authentication/Provider/UserAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/UserAuthenticationProviderTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Core\Role\SwitchUserRole; +use Symfony\Component\Security\Core\User\UserInterface; class UserAuthenticationProviderTest extends TestCase { @@ -62,6 +63,24 @@ public function testAuthenticateWhenUsernameIsNotFoundAndHideIsTrue() $provider->authenticate($this->getSupportedToken()); } + public function testAuthenticateWhenCredentialsAreInvalidAndHideIsTrue() + { + $provider = $this->getProvider(); + $provider->expects($this->once()) + ->method('retrieveUser') + ->willReturn($this->createMock(UserInterface::class)) + ; + $provider->expects($this->once()) + ->method('checkAuthentication') + ->willThrowException(new BadCredentialsException()) + ; + + $this->expectException(BadCredentialsException::class); + $this->expectExceptionMessage('Bad credentials.'); + + $provider->authenticate($this->getSupportedToken()); + } + /** * @group legacy */ From 934761537a527b96708f0447bb437ff06e7c381e Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 20 May 2021 16:01:31 +0200 Subject: [PATCH 32/35] [Security][SecurityBundle] Fix deprecations triggered in tests --- .../Provider/DaoAuthenticationProviderTest.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php b/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php index 05340afa..e60034f0 100644 --- a/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php @@ -29,13 +29,13 @@ use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +/** + * @group legacy + */ class DaoAuthenticationProviderTest extends TestCase { use ExpectDeprecationTrait; - /** - * @group legacy - */ public function testRetrieveUserWhenProviderDoesNotReturnAnUserInterface() { $this->expectException(AuthenticationServiceException::class); @@ -53,9 +53,6 @@ public function testRetrieveUserWhenProviderDoesNotReturnAnUserInterface() $method->invoke($provider, 'fabien', $this->getSupportedToken()); } - /** - * @group legacy - */ public function testRetrieveUserWhenUsernameIsNotFoundWithLegacyEncoderFactory() { $this->expectException(UserNotFoundException::class); From 2d62270081370e2e3421014b7a8e8b4149b0d2da Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 20 May 2021 16:30:25 +0200 Subject: [PATCH 33/35] [Security] Add UserAuthenticationProviderTest to legacy group --- .../Authentication/Provider/UserAuthenticationProviderTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/Authentication/Provider/UserAuthenticationProviderTest.php b/Tests/Authentication/Provider/UserAuthenticationProviderTest.php index 851758d8..c4bcd8f5 100644 --- a/Tests/Authentication/Provider/UserAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/UserAuthenticationProviderTest.php @@ -26,6 +26,9 @@ use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; +/** + * @group legacy + */ class UserAuthenticationProviderTest extends TestCase { public function testSupports() From 7c05f054a82c349f654fe9672e41323ae8afec20 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Sun, 23 May 2021 15:41:18 +0200 Subject: [PATCH 34/35] Remove deprecated User from serialized test fixture --- .../Token/Fixtures/CustomUser.php | 48 ++++++++++++++++++ .../Token/Fixtures/switch-user-token-4.4.txt | Bin 1917 -> 1319 bytes .../Token/SwitchUserTokenTest.php | 21 ++++++++ .../Token/switch-user-token-4.4.txt | Bin 1165 -> 0 bytes 4 files changed, 69 insertions(+) create mode 100644 Tests/Authentication/Token/Fixtures/CustomUser.php delete mode 100644 Tests/Authentication/Token/switch-user-token-4.4.txt diff --git a/Tests/Authentication/Token/Fixtures/CustomUser.php b/Tests/Authentication/Token/Fixtures/CustomUser.php new file mode 100644 index 00000000..52fea7a3 --- /dev/null +++ b/Tests/Authentication/Token/Fixtures/CustomUser.php @@ -0,0 +1,48 @@ +username = $username; + $this->roles = $roles; + } + + public function getUsername(): string + { + return $this->username; + } + + public function getUserIdentifier(): string + { + return $this->username; + } + + public function getRoles(): array + { + return $this->roles; + } + + public function getPassword(): ?string + { + return null; + } + + public function getSalt(): ?string + { + return null; + } + + public function eraseCredentials(): void + { + } +} diff --git a/Tests/Authentication/Token/Fixtures/switch-user-token-4.4.txt b/Tests/Authentication/Token/Fixtures/switch-user-token-4.4.txt index 7b3f7c40920dbe5d8d51326d95d3bdc721a4b2b6..fc8af1432871f11d7804a00d6f09fe38f8a6e582 100644 GIT binary patch delta 264 zcmey%x14K&FSC)c*~EZYW>Yisi5r}m%q=DtGApr$q!yPHPizpAam%bIDJ@DZj&Uw6 zF3HagElw>`vNE!&F1E6;m>kHe3|GmIrgCy1qYRUUDSkC?nbaolW7{`bl*N^*Hj7R^ K%fe2i?fU_ZXj{bq literal 1917 zcmeHI!D_-l6!cs63yj8?zMP8KlV~yZ7AR|6aci@{j?qY`JFSGuqfo?!kvTmX5j_|A247{&bE#GIrnf>x;a=wPW|0X~ z-x)Z5--VJx4@wE$U<5<=m()b6cq(3bmhH7!wF7+5BmREF&%uE*!y8*`&4T*0ZediiHS#g~;O?13H zOL8&57LtrkM8+@>m>!C01}I}bn~dKV;dqYe!ByLt6o=gK7b%ie&D({tsv}67Z?e~p zx-WZk6d2J58%5c3hj;ipfjZ=m!gk>b74^|HiId>|V83d_|Hqo?4YvJJvx~|;YIgOX Lc52L@)vWpivwNcV diff --git a/Tests/Authentication/Token/SwitchUserTokenTest.php b/Tests/Authentication/Token/SwitchUserTokenTest.php index 477247e7..e605615b 100644 --- a/Tests/Authentication/Token/SwitchUserTokenTest.php +++ b/Tests/Authentication/Token/SwitchUserTokenTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Tests\Authentication\Token\Fixtures\CustomUser; use Symfony\Component\Security\Core\User\UserInterface; class SwitchUserTokenTest extends TestCase @@ -90,6 +91,25 @@ public function testSerializeNullImpersonateUrl() $this->assertNull($unserializedToken->getOriginatedFromUri()); } + /** + * Tests if an old version of SwitchUserToken can still be unserialized. + * + * The fixture was generated by running the following code with Symfony 4.4 and PHP 7.2. + * + * serialize( + * new SwitchUserToken( + * new CustomUser('john', ['ROLE_USER']), + * ['foo' => 'bar'], + * 'main', ['ROLE_USER'], + * new UsernamePasswordToken( + * new CustomUser('jane', ['ROLE_USER']), + * ['foo' => 'bar'], + * 'main', + * ['ROLE_USER'] + * ) + * ) + * ) + */ public function testUnserializeOldToken() { /** @var SwitchUserToken $token */ @@ -97,6 +117,7 @@ public function testUnserializeOldToken() self::assertInstanceOf(SwitchUserToken::class, $token); self::assertInstanceOf(UsernamePasswordToken::class, $token->getOriginalToken()); + self::assertInstanceOf(CustomUser::class, $token->getUser()); self::assertSame('john', $token->getUserIdentifier()); self::assertSame(['foo' => 'bar'], $token->getCredentials()); self::assertSame('main', $token->getFirewallName()); diff --git a/Tests/Authentication/Token/switch-user-token-4.4.txt b/Tests/Authentication/Token/switch-user-token-4.4.txt deleted file mode 100644 index f359ec4a3ddde5cb7565e4280920ea94eab05754..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1165 zcmeHF&1%Ci4DPe|36eHxGx;<^4}+~8&3ZEoCv}Op#x8b32_f%3Da~OpXd$=4F2)l6 zeymR^EE8Z^TOF-wMQW?FHOkZ?Q$^!+O)aOyb5obt)rG9JHR8j5Ds5MH8us)W}M`OYbk%9Y%pDS^eUd5JKlsjUBCJe7NP(G2Uwku|)Ao zYQwmOIhPP$U2P$Hy6=h%h!^vwD(hM*7}B9wjM&+|Y5f7un(;s65^a4+qv$%3?L1C} z@ePq+eiJMyBlD9wFrE*?ikFjEoINSeaJm=;W$pn7wA;R}Klj;shfxe!kOYOW!E=F+ f1L&|H-GW_#1fcB3je6k3ZHbHcpZJYM>HGc%Z1|PJ From ea473c5a76f89900c908c16eb12e62ef32f848b7 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 26 May 2021 19:39:37 +0200 Subject: [PATCH 35/35] Fix CS in README files --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 70476d9e..6b3e5c99 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ the Java Spring framework. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/security.html) - * [Contributing](https://symfony.com/doc/current/contributing/index.html) - * [Report issues](https://github.com/symfony/symfony/issues) and - [send Pull Requests](https://github.com/symfony/symfony/pulls) - in the [main Symfony repository](https://github.com/symfony/symfony) + * [Documentation](https://symfony.com/doc/current/components/security.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony)