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) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy