diff --git a/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php b/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php new file mode 100644 index 0000000000..0afa2121aa --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php @@ -0,0 +1,117 @@ + + * + * 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; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\AuthenticationEvents; +use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent; +use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\AuthenticationExpiredException; +use Symfony\Component\Security\Core\Exception\ProviderNotFoundException; +use Symfony\Component\Security\Core\User\UserCheckerInterface; +use Symfony\Component\Security\Guard\AuthenticatorInterface; +use Symfony\Component\Security\Guard\Provider\GuardAuthenticationProviderTrait; +use Symfony\Component\Security\Guard\Token\GuardTokenInterface; +use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +class GuardAuthenticationManager implements AuthenticationManagerInterface +{ + use GuardAuthenticationProviderTrait; + + private $guardAuthenticators; + private $userChecker; + private $eraseCredentials; + /** @var EventDispatcherInterface */ + private $eventDispatcher; + + /** + * @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationListener + */ + public function __construct($guardAuthenticators, UserCheckerInterface $userChecker, bool $eraseCredentials = true) + { + $this->guardAuthenticators = $guardAuthenticators; + $this->userChecker = $userChecker; + $this->eraseCredentials = $eraseCredentials; + } + + public function setEventDispatcher(EventDispatcherInterface $dispatcher) + { + $this->eventDispatcher = $dispatcher; + } + + public function authenticate(TokenInterface $token) + { + if (!$token instanceof GuardTokenInterface) { + throw new \InvalidArgumentException('GuardAuthenticationManager only supports GuardTokenInterface.'); + } + + if (!$token instanceof PreAuthenticationGuardToken) { + /* + * The listener *only* passes PreAuthenticationGuardToken instances. + * This means that an authenticated token (e.g. PostAuthenticationGuardToken) + * is being passed here, which happens if that token becomes + * "not authenticated" (e.g. happens if the user changes between + * requests). In this case, the user should be logged out. + */ + + // this should never happen - but technically, the token is + // authenticated... so it could just be returned + if ($token->isAuthenticated()) { + return $token; + } + + // this AccountStatusException causes the user to be logged out + throw new AuthenticationExpiredException(); + } + + $guard = $this->findOriginatingAuthenticator($token); + if (null === $guard) { + $this->handleFailure(new ProviderNotFoundException(sprintf('Token with provider key "%s" did not originate from any of the guard authenticators.', $token->getGuardProviderKey())), $token); + } + + try { + $result = $this->authenticateViaGuard($guard, $token); + } catch (AuthenticationException $exception) { + $this->handleFailure($exception, $token); + } + + if (true === $this->eraseCredentials) { + $result->eraseCredentials(); + } + + if (null !== $this->eventDispatcher) { + $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($result), AuthenticationEvents::AUTHENTICATION_SUCCESS); + } + + return $result; + } + + private function handleFailure(AuthenticationException $exception, TokenInterface $token) + { + if (null !== $this->eventDispatcher) { + $this->eventDispatcher->dispatch(new AuthenticationFailureEvent($token, $exception), AuthenticationEvents::AUTHENTICATION_FAILURE); + } + + $exception->setToken($token); + + throw $exception; + } + + protected function getGuardKey(string $key): string + { + // Guard authenticators in the GuardAuthenticationManager are already indexed + // by an unique key + return $key; + } +} diff --git a/src/Symfony/Component/Security/Core/composer.json b/src/Symfony/Component/Security/Core/composer.json index fc500b285f..83b082bdde 100644 --- a/src/Symfony/Component/Security/Core/composer.json +++ b/src/Symfony/Component/Security/Core/composer.json @@ -20,6 +20,7 @@ "symfony/event-dispatcher-contracts": "^1.1|^2", "symfony/polyfill-php80": "^1.15", "symfony/service-contracts": "^1.1.6|^2", + "symfony/security-guard": "^4.4", "symfony/deprecation-contracts": "^2.1" }, "require-dev": { diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php index 7e9258a9c5..ac5c4cc2d4 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php @@ -16,14 +16,9 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\AuthenticationExpiredException; -use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; -use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserCheckerInterface; -use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Guard\AuthenticatorInterface; -use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; use Symfony\Component\Security\Guard\Token\GuardTokenInterface; use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; @@ -35,6 +30,8 @@ use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; */ class GuardAuthenticationProvider implements AuthenticationProviderInterface { + use GuardAuthenticationProviderTrait; + /** * @var AuthenticatorInterface[] */ @@ -99,60 +96,6 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface return $this->authenticateViaGuard($guardAuthenticator, $token); } - private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, PreAuthenticationGuardToken $token): GuardTokenInterface - { - // get the user from the GuardAuthenticator - $user = $guardAuthenticator->getUser($token->getCredentials(), $this->userProvider); - - if (null === $user) { - throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', get_debug_type($guardAuthenticator))); - } - - if (!$user instanceof UserInterface) { - throw new \UnexpectedValueException(sprintf('The "%s::getUser()" method must return a UserInterface. You returned "%s".', get_debug_type($guardAuthenticator), get_debug_type($user))); - } - - $this->userChecker->checkPreAuth($user); - if (true !== $checkCredentialsResult = $guardAuthenticator->checkCredentials($token->getCredentials(), $user)) { - if (false !== $checkCredentialsResult) { - throw new \TypeError(sprintf('"%s::checkCredentials()" must return a boolean value.', get_debug_type($guardAuthenticator))); - } - - throw new BadCredentialsException(sprintf('Authentication failed because "%s::checkCredentials()" did not return true.', get_debug_type($guardAuthenticator))); - } - if ($this->userProvider instanceof PasswordUpgraderInterface && $guardAuthenticator instanceof PasswordAuthenticatedInterface && null !== $this->passwordEncoder && (null !== $password = $guardAuthenticator->getPassword($token->getCredentials())) && method_exists($this->passwordEncoder, 'needsRehash') && $this->passwordEncoder->needsRehash($user)) { - $this->userProvider->upgradePassword($user, $this->passwordEncoder->encodePassword($user, $password)); - } - $this->userChecker->checkPostAuth($user); - - // turn the UserInterface into a TokenInterface - $authenticatedToken = $guardAuthenticator->createAuthenticatedToken($user, $this->providerKey); - if (!$authenticatedToken instanceof TokenInterface) { - throw new \UnexpectedValueException(sprintf('The "%s::createAuthenticatedToken()" method must return a TokenInterface. You returned "%s".', get_debug_type($guardAuthenticator), get_debug_type($authenticatedToken))); - } - - return $authenticatedToken; - } - - private function findOriginatingAuthenticator(PreAuthenticationGuardToken $token): ?AuthenticatorInterface - { - // find the *one* GuardAuthenticator that this token originated from - foreach ($this->guardAuthenticators as $key => $guardAuthenticator) { - // get a key that's unique to *this* guard authenticator - // this MUST be the same as GuardAuthenticationListener - $uniqueGuardKey = $this->providerKey.'_'.$key; - - if ($uniqueGuardKey === $token->getGuardProviderKey()) { - return $guardAuthenticator; - } - } - - // no matching authenticator found - but there will be multiple GuardAuthenticationProvider - // instances that will be checked if you have multiple firewalls. - - return null; - } - public function supports(TokenInterface $token) { if ($token instanceof PreAuthenticationGuardToken) { @@ -161,4 +104,9 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface return $token instanceof GuardTokenInterface; } + + protected function getGuardKey(string $key): string + { + return $this->providerKey.'_'.$key; + } } diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php new file mode 100644 index 0000000000..33e82eb022 --- /dev/null +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Guard\Provider; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Guard\AuthenticatorInterface; +use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; +use Symfony\Component\Security\Guard\Token\GuardTokenInterface; +use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; + +/** + * @author Ryan Weaver + * + * @internal + */ +trait GuardAuthenticationProviderTrait +{ + private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, PreAuthenticationGuardToken $token): GuardTokenInterface + { + // get the user from the GuardAuthenticator + $user = $guardAuthenticator->getUser($token->getCredentials(), $this->userProvider); + + if (null === $user) { + throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', get_debug_type($guardAuthenticator))); + } + + if (!$user instanceof UserInterface) { + throw new \UnexpectedValueException(sprintf('The "%s::getUser()" method must return a UserInterface. You returned "%s".', get_debug_type($guardAuthenticator), get_debug_type($user))); + } + + $this->userChecker->checkPreAuth($user); + if (true !== $checkCredentialsResult = $guardAuthenticator->checkCredentials($token->getCredentials(), $user)) { + if (false !== $checkCredentialsResult) { + throw new \TypeError(sprintf('"%s::checkCredentials()" must return a boolean value.', get_debug_type($guardAuthenticator))); + } + + throw new BadCredentialsException(sprintf('Authentication failed because "%s::checkCredentials()" did not return true.', get_debug_type($guardAuthenticator))); + } + if ($this->userProvider instanceof PasswordUpgraderInterface && $guardAuthenticator instanceof PasswordAuthenticatedInterface && null !== $this->passwordEncoder && (null !== $password = $guardAuthenticator->getPassword($token->getCredentials())) && method_exists($this->passwordEncoder, 'needsRehash') && $this->passwordEncoder->needsRehash($user)) { + $this->userProvider->upgradePassword($user, $this->passwordEncoder->encodePassword($user, $password)); + } + $this->userChecker->checkPostAuth($user); + + // turn the UserInterface into a TokenInterface + $authenticatedToken = $guardAuthenticator->createAuthenticatedToken($user, $this->providerKey); + if (!$authenticatedToken instanceof TokenInterface) { + throw new \UnexpectedValueException(sprintf('The "%s::createAuthenticatedToken()" method must return a TokenInterface. You returned "%s".', get_debug_type($guardAuthenticator), get_debug_type($authenticatedToken))); + } + + return $authenticatedToken; + } + + private function findOriginatingAuthenticator(PreAuthenticationGuardToken $token): ?AuthenticatorInterface + { + // find the *one* GuardAuthenticator that this token originated from + foreach ($this->guardAuthenticators as $key => $guardAuthenticator) { + // get a key that's unique to *this* guard authenticator + // this MUST be the same as GuardAuthenticationListener + $uniqueGuardKey = $this->getGuardKey($key); + + if ($uniqueGuardKey === $token->getGuardProviderKey()) { + return $guardAuthenticator; + } + } + + // no matching authenticator found - but there will be multiple GuardAuthenticationProvider + // instances that will be checked if you have multiple firewalls. + + return null; + } + + abstract protected function getGuardKey(string $key): string; +}