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 e91c5d81..92a48dc9 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,9 +19,13 @@ 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\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); @@ -32,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 { @@ -89,6 +96,8 @@ public function authenticate(TokenInterface $token) break; } catch (AuthenticationException $e) { $lastException = $e; + } catch (InvalidPasswordException $e) { + $lastException = new BadCredentialsException('Bad credentials.', 0, $e); } } @@ -101,6 +110,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/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 c65a9505..d83c1a0c 100644 --- a/Authentication/Provider/DaoAuthenticationProvider.php +++ b/Authentication/Provider/DaoAuthenticationProvider.php @@ -11,32 +11,46 @@ 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\Exception\UserNotFoundException; +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; +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 { - 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 +73,38 @@ protected function checkAuthentication(UserInterface $user, UsernamePasswordToke throw new BadCredentialsException('The presented password is invalid.'); } - $encoder = $this->encoderFactory->getEncoder($user); + 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)); + } - if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) { + // deprecated since Symfony 5.3 + if ($this->hasherFactory instanceof EncoderFactoryInterface) { + $encoder = $this->hasherFactory->getEncoder($user); + + if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $salt)) { + 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 (!$hasher->verify($user->getPassword(), $presentedPassword, $salt)) { 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, $salt)); } } } @@ -74,7 +112,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) { @@ -82,15 +120,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..418523e2 100644 --- a/Authentication/Provider/LdapBindAuthenticationProvider.php +++ b/Authentication/Provider/LdapBindAuthenticationProvider.php @@ -16,11 +16,13 @@ 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; +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 { @@ -38,7 +42,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 +54,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 +64,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 +85,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 +100,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 +109,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..4f69f33a 100644 --- a/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php +++ b/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php @@ -18,15 +18,19 @@ 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. * * 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 + * + * @deprecated since Symfony 5.3, use the new authenticator system instead */ class PreAuthenticatedAuthenticationProvider implements AuthenticationProviderInterface { @@ -54,7 +58,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..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; @@ -51,7 +56,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 d48af5d8..cf228c7e 100644 --- a/Authentication/Provider/UserAuthenticationProvider.php +++ b/Authentication/Provider/UserAuthenticationProvider.php @@ -18,14 +18,19 @@ 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\CustomUserMessageAccountStatusException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; 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 { @@ -56,18 +61,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; } @@ -80,8 +85,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 (AccountStatusException | BadCredentialsException $e) { + if ($this->hideUserNotFoundExceptions && !$e instanceof CustomUserMessageAccountStatusException) { throw new BadCredentialsException('Bad credentials.', 0, $e); } 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/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/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/Authentication/Token/AbstractToken.php b/Authentication/Token/AbstractToken.php index 9106334b..a68a27d4 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; /** @@ -50,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 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)); + } + 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; @@ -233,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)); } /** @@ -262,10 +285,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; } @@ -280,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 b8f1c463..28c77d75 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 ''; } @@ -96,6 +103,9 @@ public function __unserialize(array $data): void /** * @return string + * + * @internal in 5.3 + * @final in 5.3 */ public function serialize() { @@ -104,6 +114,9 @@ public function serialize() /** * @return void + * + * @internal in 5.3 + * @final in 5.3 */ public function unserialize($serialized) { 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/Storage/UsageTrackingTokenStorage.php b/Authentication/Token/Storage/UsageTrackingTokenStorage.php index b90d5ab2..4b2cac74 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; } /** @@ -38,9 +39,9 @@ public function __construct(TokenStorageInterface $storage, ContainerInterface $ */ public function getToken(): ?TokenInterface { - if ($this->enableUsageTracking) { + if ($this->shouldTrackUsage()) { // increments the internal session usage index - $this->sessionLocator->get('session')->getMetadataBag(); + $this->getSession()->getMetadataBag(); } return $this->storage->getToken(); @@ -53,9 +54,9 @@ 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->sessionLocator->get('session')->getMetadataBag(); + $this->getSession()->getMetadataBag(); } } @@ -72,7 +73,39 @@ 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(); + } + + 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/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/Authorization/AccessDecisionManager.php b/Authorization/AccessDecisionManager.php index 440eac75..82f9e0ae 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/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 diff --git a/Encoder/BasePasswordEncoder.php b/Encoder/BasePasswordEncoder.php index e067a48a..21c59b3c 100644 --- a/Encoder/BasePasswordEncoder.php +++ b/Encoder/BasePasswordEncoder.php @@ -11,10 +11,16 @@ namespace Symfony\Component\Security\Core\Encoder; +use Symfony\Component\PasswordHasher\Hasher\CheckPasswordLengthTrait; + +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. * * @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..526c461e 100644 --- a/Encoder/EncoderFactory.php +++ b/Encoder/EncoderFactory.php @@ -11,12 +11,20 @@ namespace Symfony\Component\Security\Core\Encoder; +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); + /** * A generic encoder factory implementation. * * @author Johannes M. Schmitt + * + * @deprecated since Symfony 5.3, use {@link PasswordHasherFactory} instead */ class EncoderFactory implements EncoderFactoryInterface { @@ -34,7 +42,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)); } @@ -54,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/EncoderFactoryInterface.php b/Encoder/EncoderFactoryInterface.php index 2b9834b6..83dea6c7 100644 --- a/Encoder/EncoderFactoryInterface.php +++ b/Encoder/EncoderFactoryInterface.php @@ -11,12 +11,17 @@ namespace Symfony\Component\Security\Core\Encoder; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; use Symfony\Component\Security\Core\User\UserInterface; +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. * * @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/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/MessageDigestPasswordEncoder.php b/Encoder/MessageDigestPasswordEncoder.php index d769f2f4..8ea18c05 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; +use Symfony\Component\PasswordHasher\Hasher\MessageDigestPasswordHasher; + +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. * * @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..53d3a58d 100644 --- a/Encoder/MigratingPasswordEncoder.php +++ b/Encoder/MigratingPasswordEncoder.php @@ -11,6 +11,10 @@ namespace Symfony\Component\Security\Core\Encoder; +use Symfony\Component\PasswordHasher\Hasher\MigratingPasswordHasher; + +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. * Validates them using a chain of encoders. @@ -19,6 +23,8 @@ * 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 { diff --git a/Encoder/NativePasswordEncoder.php b/Encoder/NativePasswordEncoder.php index 5748dd5c..bc135bb1 100644 --- a/Encoder/NativePasswordEncoder.php +++ b/Encoder/NativePasswordEncoder.php @@ -11,7 +11,9 @@ namespace Symfony\Component\Security\Core\Encoder; -use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher; + +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', NativePasswordEncoder::class, NativePasswordHasher::class); /** * Hashes passwords using password_hash(). @@ -19,105 +21,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'] = \PASSWORD_ARGON2I; - } - - if (\defined('PASSWORD_ARGON2ID')) { - $this->algo = $algos[3] = $algos['argon2id'] = \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 || (\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..45aa24ed 100644 --- a/Encoder/PasswordEncoderInterface.php +++ b/Encoder/PasswordEncoderInterface.php @@ -11,12 +11,17 @@ namespace Symfony\Component\Security\Core\Encoder; +use Symfony\Component\PasswordHasher\PasswordHasherInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; +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. * * @author Fabien Potencier + * + * @deprecated since Symfony 5.3, use {@link PasswordHasherInterface} instead */ interface PasswordEncoderInterface { 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/Encoder/Pbkdf2PasswordEncoder.php b/Encoder/Pbkdf2PasswordEncoder.php index ab5e1a53..d92c12fc 100644 --- a/Encoder/Pbkdf2PasswordEncoder.php +++ b/Encoder/Pbkdf2PasswordEncoder.php @@ -11,7 +11,9 @@ namespace Symfony\Component\Security\Core\Encoder; -use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher; + +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). @@ -25,14 +27,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 +42,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..497e9f19 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; +use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher; + +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. @@ -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..b8740bc9 100644 --- a/Encoder/SelfSaltingEncoderInterface.php +++ b/Encoder/SelfSaltingEncoderInterface.php @@ -11,11 +11,17 @@ namespace Symfony\Component\Security\Core\Encoder; +use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface; + +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 * 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..d2d71f48 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; +use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher; + +trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', SodiumPasswordEncoder::class, SodiumPasswordHasher::class); /** * 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..7b29918c 100644 --- a/Encoder/UserPasswordEncoder.php +++ b/Encoder/UserPasswordEncoder.php @@ -11,12 +11,19 @@ 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', 'The "%s" class is deprecated, use "%s" instead.', UserPasswordEncoder::class, UserPasswordHasher::class); + /** * A generic password encoder. * * @author Ariel Ferrandini + * + * @deprecated since Symfony 5.3, use {@link UserPasswordHasher} instead */ class UserPasswordEncoder implements UserPasswordEncoderInterface { @@ -34,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/Encoder/UserPasswordEncoderInterface.php b/Encoder/UserPasswordEncoderInterface.php index 522ec0b0..488777c1 100644 --- a/Encoder/UserPasswordEncoderInterface.php +++ b/Encoder/UserPasswordEncoderInterface.php @@ -11,12 +11,17 @@ namespace Symfony\Component\Security\Core\Encoder; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Security\Core\User\UserInterface; +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. * * @author Ariel Ferrandini + * + * @deprecated since Symfony 5.3, use {@link UserPasswordHasherInterface} instead */ interface UserPasswordEncoderInterface { 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/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/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) diff --git a/Resources/translations/security.bg.xlf b/Resources/translations/security.bg.xlf index 318f7d49..1d45b28c 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% минути. + 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ų. + 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% минута. + 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. + 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% хв. + 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%分鐘後再試。 + 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/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..5421c77e --- /dev/null +++ b/Signature/ExpiredSignatureStorage.php @@ -0,0 +1,53 @@ + + * + * 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 + * + * @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/Authentication/AuthenticationProviderManagerTest.php b/Tests/Authentication/AuthenticationProviderManagerTest.php index db1e3887..661ffa45 100644 --- a/Tests/Authentication/AuthenticationProviderManagerTest.php +++ b/Tests/Authentication/AuthenticationProviderManagerTest.php @@ -23,7 +23,11 @@ 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; +/** + * @group legacy + */ class AuthenticationProviderManagerTest extends TestCase { public function testAuthenticateWithoutProviders() @@ -90,9 +94,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 +113,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/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/DaoAuthenticationProviderTest.php b/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php index 57ed2d0b..e60034f0 100644 --- a/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php @@ -12,41 +12,51 @@ 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; 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\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\User; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +/** + * @group legacy + */ class DaoAuthenticationProviderTest extends TestCase { + use ExpectDeprecationTrait; + 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()); } - public function testRetrieveUserWhenUsernameIsNotFound() + 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'); @@ -55,16 +65,28 @@ public function testRetrieveUserWhenUsernameIsNotFound() $method->invoke($provider, 'fabien', $this->getSupportedToken()); } + public function testRetrieveUserWhenUsernameIsNotFound() + { + $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'); + $method->setAccessible(true); + + $method->invoke($provider, 'fabien', $this->getSupportedToken()); + } + 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()) ; - $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); @@ -73,9 +95,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(); @@ -85,7 +107,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); @@ -95,31 +117,25 @@ 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(EncoderFactoryInterface::class)); + $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() { $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 +151,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); @@ -155,7 +171,7 @@ public function testCheckAuthenticationWhenCredentialsAre0() $method->invoke( $provider, - new User('username', 'password'), + new InMemoryUser('username', 'password'), $token ); } @@ -163,13 +179,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); @@ -179,7 +195,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() @@ -235,13 +251,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); @@ -251,28 +267,28 @@ 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'); - $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,32 +320,34 @@ protected function getSupportedToken() return $mock; } - protected function getProvider($user = null, $userChecker = null, $passwordEncoder = 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) { $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); } } @@ -355,10 +373,22 @@ public function getUsername(): string return 'jane_doe'; } + public function getUserIdentifier(): string + { + return 'jane_doe'; + } + 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 0605df44..27dc2acc 100644 --- a/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php @@ -20,12 +20,14 @@ 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\InMemoryUserProvider; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; /** * @requires extension ldap + * @group legacy */ class LdapBindAuthenticationProviderTest extends TestCase { @@ -41,7 +43,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 +58,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() @@ -70,21 +72,22 @@ 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); $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() { - $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); @@ -136,7 +139,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 +181,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() @@ -207,6 +210,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$'); @@ -214,6 +218,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/PreAuthenticatedAuthenticationProviderTest.php b/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php index a0d60413..f7f5fb45 100644 --- a/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php @@ -18,10 +18,14 @@ 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; +/** + * @group legacy + */ class PreAuthenticatedAuthenticationProviderTest extends TestCase { public function testSupports() @@ -120,10 +124,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/RememberMeAuthenticationProviderTest.php b/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php index d5bd2d40..9a6a417b 100644 --- a/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php @@ -19,10 +19,13 @@ 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; +/** + * @group legacy + */ class RememberMeAuthenticationProviderTest extends TestCase { public function testSupports() @@ -59,7 +62,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/Provider/UserAuthenticationProviderTest.php b/Tests/Authentication/Provider/UserAuthenticationProviderTest.php index dae32f54..c4bcd8f5 100644 --- a/Tests/Authentication/Provider/UserAuthenticationProviderTest.php +++ b/Tests/Authentication/Provider/UserAuthenticationProviderTest.php @@ -21,10 +21,14 @@ 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; +/** + * @group legacy + */ class UserAuthenticationProviderTest extends TestCase { public function testSupports() @@ -46,11 +50,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,12 +66,30 @@ public function testAuthenticateWhenUsernameIsNotFoundAndHideIsTrue() $provider = $this->getProvider(false, true); $provider->expects($this->once()) ->method('retrieveUser') - ->willThrowException(new UsernameNotFoundException()) + ->willThrowException(new UserNotFoundException()) ; $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()); + } + public function testAuthenticateWhenProviderDoesNotReturnAnUserInterface() { $this->expectException(AuthenticationServiceException::class); @@ -194,7 +216,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/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/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/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 7b3f7c40..fc8af143 100644 Binary files a/Tests/Authentication/Token/Fixtures/switch-user-token-4.4.txt and b/Tests/Authentication/Token/Fixtures/switch-user-token-4.4.txt differ diff --git a/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php b/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php index 607ccc75..0d074bd4 100644 --- a/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php +++ b/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php @@ -13,25 +13,39 @@ 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(['session' => function () use (&$sessionAccess) { - ++$sessionAccess; - + $sessionLocator = new class(['request_stack' => function () use (&$sessionAccess) { $session = $this->createMock(SessionInterface::class); - $session->expects($this->once()) - ->method('getMetadataBag'); - return $session; + $request = new Request(); + $request->setSession($session); + $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 { use ServiceLocatorTrait; }; @@ -39,7 +53,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()); @@ -54,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(); + } } diff --git a/Tests/Authentication/Token/SwitchUserTokenTest.php b/Tests/Authentication/Token/SwitchUserTokenTest.php index 8138f765..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 @@ -26,7 +27,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 +36,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 +50,11 @@ public function getUsername() return 'impersonated'; } + public function getUserIdentifier() + { + return 'impersonated'; + } + public function getPassword() { return null; @@ -85,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 */ @@ -92,7 +117,8 @@ public function testUnserializeOldToken() self::assertInstanceOf(SwitchUserToken::class, $token); self::assertInstanceOf(UsernamePasswordToken::class, $token->getOriginalToken()); - self::assertSame('john', $token->getUsername()); + self::assertInstanceOf(CustomUser::class, $token->getUser()); + 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/Authorization/AccessDecisionManagerTest.php b/Tests/Authorization/AccessDecisionManagerTest.php index 5f2e5d65..375fb6d6 100644 --- a/Tests/Authorization/AccessDecisionManagerTest.php +++ b/Tests/Authorization/AccessDecisionManagerTest.php @@ -12,12 +12,15 @@ namespace Symfony\Component\Security\Core\Tests\Authorization; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 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::class); @@ -35,6 +38,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->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".'); + + $manager->decide($token, ['ROLE_FOO']); + } + public function getStrategyTests() { return [ @@ -95,6 +112,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 = []; 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/Encoder/EncoderFactoryTest.php b/Tests/Encoder/EncoderFactoryTest.php index a6999991..7b05c9be 100644 --- a/Tests/Encoder/EncoderFactoryTest.php +++ b/Tests/Encoder/EncoderFactoryTest.php @@ -12,15 +12,24 @@ 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; +/** + * @group legacy + */ class EncoderFactoryTest extends TestCase { public function testGetEncoderWithMessageDigestEncoder() @@ -176,6 +185,39 @@ 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', '')); + } + + 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 @@ -196,6 +238,10 @@ public function getUsername(): string { } + public function getUserIdentifier(): string + { + } + public function eraseCredentials() { } @@ -214,3 +260,13 @@ 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/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/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'])); } } 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/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')); + } +} diff --git a/Tests/User/ChainUserProviderTest.php b/Tests/User/ChainUserProviderTest.php index b7e2a411..5a477006 100644 --- a/Tests/User/ChainUserProviderTest.php +++ b/Tests/User/ChainUserProviderTest.php @@ -13,10 +13,12 @@ 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\User; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -24,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') @@ -89,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') @@ -108,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') @@ -118,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') @@ -141,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') @@ -154,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') @@ -173,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') @@ -181,7 +183,7 @@ public function testSupportsClass() ->willReturn(false) ; - $provider2 = $this->createMock(UserProviderInterface::class); + $provider2 = $this->createMock(InMemoryUserProvider::class); $provider2 ->expects($this->once()) ->method('supportsClass') @@ -195,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') @@ -203,7 +205,7 @@ public function testSupportsClassWhenNotSupported() ->willReturn(false) ; - $provider2 = $this->createMock(UserProviderInterface::class); + $provider2 = $this->createMock(InMemoryUserProvider::class); $provider2 ->expects($this->once()) ->method('supportsClass') @@ -217,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') @@ -230,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') @@ -249,16 +251,16 @@ public function testAcceptsTraversable() public function testPasswordUpgrades() { - $user = new User('user', 'pwd'); + $user = new InMemoryUser('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 +271,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/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..d4d4964c 100644 --- a/Tests/User/InMemoryUserProviderTest.php +++ b/Tests/User/InMemoryUserProviderTest.php @@ -12,23 +12,42 @@ namespace Symfony\Component\Security\Core\Tests\User; use PHPUnit\Framework\TestCase; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +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; class InMemoryUserProviderTest extends TestCase { + use ExpectDeprecationTrait; + 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()); } 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'); @@ -55,9 +74,9 @@ 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'); + $user = $provider->loadUserByIdentifier('fabien'); $this->assertEquals('foo', $user->getPassword()); } @@ -65,14 +84,14 @@ 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() { - $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 new file mode 100644 index 00000000..a5496ef3 --- /dev/null +++ b/Tests/User/InMemoryUserTest.php @@ -0,0 +1,119 @@ + + * + * 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\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); + 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()); + } + + /** + * @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'); + $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..81b8705d 100644 --- a/Tests/User/UserTest.php +++ b/Tests/User/UserTest.php @@ -12,12 +12,18 @@ 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; +/** + * @group legacy + */ class UserTest extends TestCase { + use ExpectDeprecationTrait; + public function testConstructorException() { $this->expectException(\InvalidArgumentException::class); @@ -39,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 23321250..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; } @@ -73,22 +87,24 @@ 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; } 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))); @@ -110,10 +126,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/InMemoryUser.php b/User/InMemoryUser.php new file mode 100644 index 00000000..39da71e3 --- /dev/null +++ b/User/InMemoryUser.php @@ -0,0 +1,76 @@ + + * + * 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 extends User +{ + /** + * {@inheritdoc} + * + * @deprecated since Symfony 5.3 + */ + public function isAccountNonExpired(): bool + { + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, you should stop using it.', __METHOD__); + + return parent::isAccountNonExpired(); + } + + /** + * {@inheritdoc} + * + * @deprecated since Symfony 5.3 + */ + public function isAccountNonLocked(): bool + { + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, you should stop using it.', __METHOD__); + + return parent::isAccountNonLocked(); + } + + /** + * {@inheritdoc} + * + * @deprecated since Symfony 5.3 + */ + public function isCredentialsNonExpired(): bool + { + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, you should stop using it.', __METHOD__); + + return parent::isCredentialsNonExpired(); + } + + /** + * @deprecated since Symfony 5.3 + */ + public function getExtraFields(): array + { + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, you should stop using it.', __METHOD__); + + return parent::getExtraFields(); + } + + public function setPassword(string $password) + { + trigger_deprecation('symfony/security-core', '5.3', 'Method "%s()" is deprecated, you should stop using it.', __METHOD__); + + parent::setPassword($password); + } +} diff --git a/User/InMemoryUserChecker.php b/User/InMemoryUserChecker.php new file mode 100644 index 00000000..6f661c76 --- /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::class === \get_class($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::class !== \get_class($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..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. @@ -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); } @@ -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 User($user->getUsername(), $user->getPassword(), $user->getRoles(), $user->isEnabled(), $user->isAccountNonExpired(), $user->isCredentialsNonExpired(), $user->isAccountNonLocked()); + 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()); } /** @@ -73,13 +83,30 @@ 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()); + // @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)) { + if (User::class !== \get_class($storedUser)) { + $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($userIdentifier, $storedUser->getPassword(), $storedUser->getRoles(), $storedUser->isEnabled(), $accountNonExpired, $credentialsNonExpired, $accountNonLocked); + } - return new User($storedUser->getUsername(), $storedUser->getPassword(), $storedUser->getRoles(), $storedUser->isEnabled(), $storedUser->isAccountNonExpired(), $storedUser->isCredentialsNonExpired() && $storedUser->getPassword() === $user->getPassword(), $storedUser->isAccountNonLocked()); + return new InMemoryUser($userIdentifier, $storedUser->getPassword(), $storedUser->getRoles(), $storedUser->isEnabled()); } /** @@ -87,19 +114,24 @@ 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; } /** * 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): User + 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/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/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/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 9c65298b..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 encoded password of a user, typically for using a better hash algorithm. - * - * This method should persist the new password in the user storage and update the $user object accordingly. - * Because you don't want your users not being able to log in, this method should be opportunistic: - * it's fine if it does nothing or if it fails without throwing any exception. - */ - public function upgradePassword(UserInterface $user, string $newEncodedPassword): void; } diff --git a/User/User.php b/User/User.php index fa5cfb52..d583e5a8 100644 --- a/User/User.php +++ b/User/User.php @@ -17,8 +17,10 @@ * 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, EquatableInterface +class User implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface { private $username; private $password; @@ -31,6 +33,10 @@ final class User implements UserInterface, EquatableInterface 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.'); } @@ -47,7 +53,7 @@ public function __construct(?string $username, ?string $password, array $roles = public function __toString(): string { - return $this->getUsername(); + return $this->getUserIdentifier(); } /** @@ -78,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; } @@ -178,20 +194,22 @@ public function isEqualTo(UserInterface $user): bool return false; } - if ($this->getUsername() !== $user->getUsername()) { + if ($this->getUserIdentifier() !== $user->getUserIdentifier()) { 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()) { 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; - } } } diff --git a/User/UserInterface.php b/User/UserInterface.php index 239eb0ed..6448ab51 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. * @@ -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 @@ -49,29 +51,26 @@ 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. + * + * This method is deprecated since Symfony 5.3, implement it from {@link PasswordAuthenticatedUserInterface} instead. * - * @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. + * + * This method is deprecated since Symfony 5.3, implement it from {@link LegacyPasswordAuthenticatedUserInterface} instead. * * @return string|null The salt */ 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); diff --git a/Validator/Constraints/UserPasswordValidator.php b/Validator/Constraints/UserPasswordValidator.php index 24b03248..bf273f2f 100644 --- a/Validator/Constraints/UserPasswordValidator.php +++ b/Validator/Constraints/UserPasswordValidator.php @@ -11,8 +11,12 @@ 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\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -22,12 +26,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 +62,18 @@ public function validate($password, Constraint $constraint) throw new ConstraintDefinitionException('The User object must implement the UserInterface interface.'); } - $encoder = $this->encoderFactory->getEncoder($user); + 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() || !$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 177896a4..d129ffee 100644 --- a/composer.json +++ b/composer.json @@ -20,13 +20,16 @@ "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|^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": "^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 +37,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" 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