diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 57ecde2068..e4ef468c88 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -156,8 +156,8 @@ class SecurityExtension extends Extension implements PrependExtensionInterface ->replaceArgument(2, $this->statelessFirewallKeys); if ($this->authenticatorManagerEnabled) { - $container->getDefinition('security.authenticator_handler') - ->replaceArgument(2, $this->statelessFirewallKeys); + $container->getDefinition(SessionListener::class) + ->replaceArgument(1, $this->statelessFirewallKeys); } if ($config['encoders']) { @@ -444,25 +444,19 @@ class SecurityExtension extends Extension implements PrependExtensionInterface return new Reference($id); }, $firewallAuthenticationProviders); $container - ->setDefinition($managerId = 'security.authenticator.manager.'.$id, new ChildDefinition('security.authentication.manager.authenticator')) + ->setDefinition($managerId = 'security.authenticator.manager.'.$id, new ChildDefinition('security.authenticator.manager')) ->replaceArgument(0, $authenticators) + ->replaceArgument(3, $id) + ->addTag('monolog.logger', ['channel' => 'security']) ; $managerLocator = $container->getDefinition('security.authenticator.managers_locator'); $managerLocator->replaceArgument(0, array_merge($managerLocator->getArgument(0), [$id => new ServiceClosureArgument(new Reference($managerId))])); // authenticator manager listener - $container - ->setDefinition('security.firewall.authenticator.'.$id.'.locator', new ChildDefinition('security.firewall.authenticator.locator')) - ->setArguments([$authenticators]) - ->addTag('container.service_locator') - ; - $container ->setDefinition('security.firewall.authenticator.'.$id, new ChildDefinition('security.firewall.authenticator')) ->replaceArgument(0, new Reference($managerId)) - ->replaceArgument(2, new Reference('security.firewall.authenticator.'.$id.'.locator')) - ->replaceArgument(3, $id) ; $listeners[] = new Reference('security.firewall.authenticator.'.$id); diff --git a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php b/src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php deleted file mode 100644 index e4299bcc0c..0000000000 --- a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\EventListener; - -use Psr\Log\LoggerInterface; -use Symfony\Component\DependencyInjection\ServiceLocator; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; -use Symfony\Component\Security\Http\Authentication\AuthenticatorHandler; -use Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener; - -/** - * @author Wouter de Jong - * - * @experimental in 5.1 - */ -class LazyAuthenticatorManagerListener extends AuthenticatorManagerListener -{ - private $authenticatorLocator; - - public function __construct( - AuthenticationManagerInterface $authenticationManager, - AuthenticatorHandler $authenticatorHandler, - ServiceLocator $authenticatorLocator, - string $providerKey, - EventDispatcherInterface $eventDispatcher, - ?LoggerInterface $logger = null - ) { - parent::__construct($authenticationManager, $authenticatorHandler, [], $providerKey, $eventDispatcher, $logger); - - $this->authenticatorLocator = $authenticatorLocator; - } - - protected function getSupportingAuthenticators(Request $request): array - { - $authenticators = []; - $lazy = true; - foreach ($this->authenticatorLocator->getProvidedServices() as $key => $type) { - $authenticator = $this->authenticatorLocator->get($key); - if (null !== $this->logger) { - $this->logger->debug('Checking support on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); - } - - if (false !== $supports = $authenticator->supports($request)) { - $authenticators[$key] = $authenticator; - $lazy = $lazy && null === $supports; - } elseif (null !== $this->logger) { - $this->logger->debug('Guard authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); - } - } - - return [$authenticators, $lazy]; - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml index 4bfd1229a8..c9bb06d179 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml @@ -8,7 +8,7 @@ @@ -18,7 +18,7 @@ - + - authenticators + + provider key + %security.authentication.manager.erase_credentials% - - - - + + + + - - - - - - - - - - - - authenticator manager - - - - - @@ -75,6 +58,12 @@ + + + + stateless firewall keys + + diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareAuthenticatorManager.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareAuthenticatorManager.php deleted file mode 100644 index a3974dd2b3..0000000000 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareAuthenticatorManager.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\Security; - -use Symfony\Component\DependencyInjection\ServiceLocator; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Exception\LogicException; - -/** - * A decorator that delegates all method calls to the authenticator - * manager of the current firewall. - * - * @author Wouter de Jong - */ -class FirewallAwareAuthenticatorManager implements AuthenticationManagerInterface -{ - private $firewallMap; - private $authenticatorManagers; - private $requestStack; - - public function __construct(FirewallMap $firewallMap, ServiceLocator $authenticatorManagers, RequestStack $requestStack) - { - $this->firewallMap = $firewallMap; - $this->authenticatorManagers = $authenticatorManagers; - $this->requestStack = $requestStack; - } - - public function authenticate(TokenInterface $token) - { - $firewallConfig = $this->firewallMap->getFirewallConfig($this->requestStack->getMasterRequest()); - if (null === $firewallConfig) { - throw new LogicException('Cannot call authenticate on this request, as it is not behind a firewall.'); - } - - return $this->authenticatorManagers->get($firewallConfig->getName())->authenticate($token); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Security/UserAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Security/UserAuthenticator.php new file mode 100644 index 0000000000..ab2dded798 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Security/UserAuthenticator.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Security; + +use Psr\Container\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Exception\LogicException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; + +/** + * A decorator that delegates all method calls to the authenticator + * manager of the current firewall. + * + * @author Wouter de Jong + * + * @final + * @experimental in Symfony 5.1 + */ +class UserAuthenticator implements UserAuthenticatorInterface +{ + private $firewallMap; + private $userAuthenticators; + private $requestStack; + + public function __construct(FirewallMap $firewallMap, ContainerInterface $userAuthenticators, RequestStack $requestStack) + { + $this->firewallMap = $firewallMap; + $this->userAuthenticators = $userAuthenticators; + $this->requestStack = $requestStack; + } + + public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request): ?Response + { + return $this->getUserAuthenticator()->authenticateUser($user, $authenticator, $request); + } + + private function getUserAuthenticator(): UserAuthenticatorInterface + { + $firewallConfig = $this->firewallMap->getFirewallConfig($this->requestStack->getMasterRequest()); + if (null === $firewallConfig) { + throw new LogicException('Cannot call authenticate on this request, as it is not behind a firewall.'); + } + + return $this->userAuthenticators->get($firewallConfig->getName()); + } +} diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php index 37665d4fa8..5ac7935f31 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -19,8 +19,8 @@ use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterfac use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Guard\AuthenticatorInterface; -use Symfony\Component\Security\Guard\GuardHandler; -use Symfony\Component\Security\Guard\Token\PreAuthenticationToken as GuardPreAuthenticationGuardToken; +use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; +use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken as GuardPreAuthenticationGuardToken; use Symfony\Component\Security\Http\Firewall\AbstractListener; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; @@ -45,7 +45,7 @@ class GuardAuthenticationListener extends AbstractListener * @param string $providerKey The provider (i.e. firewall) key * @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationProvider */ - public function __construct(GuardHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, string $providerKey, iterable $guardAuthenticators, LoggerInterface $logger = null) + public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, string $providerKey, iterable $guardAuthenticators, LoggerInterface $logger = null) { if (empty($providerKey)) { throw new \InvalidArgumentException('$providerKey must not be empty.'); @@ -121,7 +121,7 @@ class GuardAuthenticationListener extends AbstractListener protected function executeGuardAuthenticators(array $guardAuthenticators, RequestEvent $event): void { foreach ($guardAuthenticators as $key => $guardAuthenticator) { - $uniqueGuardKey = $this->providerKey.'_'.$key;; + $uniqueGuardKey = $this->providerKey.'_'.$key; $this->executeGuardAuthenticator($uniqueGuardKey, $guardAuthenticator, $event); diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorHandler.php b/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php similarity index 65% rename from src/Symfony/Component/Security/Http/Authentication/AuthenticatorHandler.php rename to src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php index 7a579a9b2c..11f207a9ab 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorHandler.php +++ b/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php @@ -9,32 +9,30 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Http\Authentication; +namespace Symfony\Component\Security\Guard; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Guard\AuthenticatorInterface as GuardAuthenticatorInterface; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; use Symfony\Component\Security\Http\SecurityEvents; use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** - * A utility class that does much of the *work* during the authentication process. + * A utility class that does much of the *work* during the guard authentication process. * * By having the logic here instead of the listener, more of the process * can be called directly (e.g. for manual authentication) or overridden. * * @author Ryan Weaver * - * @internal + * @final */ -class AuthenticatorHandler +class GuardAuthenticatorHandler { private $tokenStorage; private $dispatcher; @@ -66,38 +64,26 @@ class AuthenticatorHandler } /** - * Returns the "on success" response for the given Authenticator. - * - * @param AuthenticatorInterface|GuardAuthenticatorInterface $authenticator + * Returns the "on success" response for the given GuardAuthenticator. */ - public function handleAuthenticationSuccess(TokenInterface $token, Request $request, $authenticator, string $providerKey): ?Response + public function handleAuthenticationSuccess(TokenInterface $token, Request $request, AuthenticatorInterface $guardAuthenticator, string $providerKey): ?Response { - if (!$authenticator instanceof AuthenticatorInterface && !$authenticator instanceof GuardAuthenticatorInterface) { - throw new \UnexpectedValueException('Invalid authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); - } - - $response = $authenticator->onAuthenticationSuccess($request, $token, $providerKey); + $response = $guardAuthenticator->onAuthenticationSuccess($request, $token, $providerKey); // check that it's a Response or null if ($response instanceof Response || null === $response) { return $response; } - throw new \UnexpectedValueException(sprintf('The "%s::onAuthenticationSuccess()" method must return null or a Response object. You returned "%s".', \get_class($authenticator), \is_object($response) ? \get_class($response) : \gettype($response))); + throw new \UnexpectedValueException(sprintf('The "%s::onAuthenticationSuccess()" method must return null or a Response object. You returned "%s".', \get_class($guardAuthenticator), get_debug_type($response))); } /** * Convenience method for authenticating the user and returning the * Response *if any* for success. - * - * @param AuthenticatorInterface|GuardAuthenticatorInterface $authenticator */ - public function authenticateUserAndHandleSuccess(UserInterface $user, Request $request, $authenticator, string $providerKey): ?Response + public function authenticateUserAndHandleSuccess(UserInterface $user, Request $request, AuthenticatorInterface $authenticator, string $providerKey): ?Response { - if (!$authenticator instanceof AuthenticatorInterface && !$authenticator instanceof GuardAuthenticatorInterface) { - throw new \UnexpectedValueException('Invalid authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); - } - // create an authenticated token for the User $token = $authenticator->createAuthenticatedToken($user, $providerKey); // authenticate this in the system @@ -110,22 +96,16 @@ class AuthenticatorHandler /** * Handles an authentication failure and returns the Response for the * GuardAuthenticator. - * - * @param AuthenticatorInterface|GuardAuthenticatorInterface $authenticator */ - public function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, $authenticator, string $providerKey): ?Response + public function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $guardAuthenticator, string $providerKey): ?Response { - if (!$authenticator instanceof AuthenticatorInterface && !$authenticator instanceof GuardAuthenticatorInterface) { - throw new \UnexpectedValueException('Invalid authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); - } - - $response = $authenticator->onAuthenticationFailure($request, $authenticationException); + $response = $guardAuthenticator->onAuthenticationFailure($request, $authenticationException); if ($response instanceof Response || null === $response) { // returning null is ok, it means they want the request to continue return $response; } - throw new \UnexpectedValueException(sprintf('The "%s::onAuthenticationFailure()" method must return null or a Response object. You returned "%s".', \get_class($authenticator), get_debug_type($response))); + throw new \UnexpectedValueException(sprintf('The "%s::onAuthenticationFailure()" method must return null or a Response object. You returned "%s".', \get_class($guardAuthenticator), get_debug_type($response))); } /** diff --git a/src/Symfony/Component/Security/Guard/GuardHandler.php b/src/Symfony/Component/Security/Guard/GuardHandler.php deleted file mode 100644 index 73e5a6e882..0000000000 --- a/src/Symfony/Component/Security/Guard/GuardHandler.php +++ /dev/null @@ -1,28 +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\Guard; - -use Symfony\Component\Security\Http\Authentication\AuthenticatorHandler; - -/** - * A utility class that does much of the *work* during the guard authentication process. - * - * By having the logic here instead of the listener, more of the process - * can be called directly (e.g. for manual authentication) or overridden. - * - * @author Ryan Weaver - * - * @final - */ -class GuardHandler extends AuthenticatorHandler -{ -} diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php index 246d5173f1..0f8287ccc2 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php @@ -11,25 +11,21 @@ namespace Symfony\Component\Security\Guard\Provider; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\RequestEvent; -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\PasswordAuthenticatedInterface; use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; 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\PreAuthenticationToken; -use Symfony\Component\Security\Http\Authentication\AuthenticatorManagerTrait; +use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; /** @@ -40,8 +36,6 @@ use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; */ class GuardAuthenticationProvider implements AuthenticationProviderInterface { - use AuthenticatorManagerTrait; - /** * @var AuthenticatorInterface[] */ @@ -78,7 +72,7 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface throw new \InvalidArgumentException('GuardAuthenticationProvider only supports GuardTokenInterface.'); } - if (!$token instanceof PreAuthenticationToken) { + if (!$token instanceof PreAuthenticationGuardToken) { /* * The listener *only* passes PreAuthenticationGuardToken instances. * This means that an authenticated token (e.g. PostAuthenticationGuardToken) @@ -101,7 +95,7 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface $guardAuthenticator = $this->findOriginatingAuthenticator($token); if (null === $guardAuthenticator) { - throw new AuthenticationException(sprintf('Token with provider key "%s" did not originate from any of the guard authenticators of provider "%s".', $token->getAuthenticatorKey(), $this->providerKey)); + throw new AuthenticationException(sprintf('Token with provider key "%s" did not originate from any of the guard authenticators of provider "%s".', $token->getGuardProviderKey(), $this->providerKey)); } return $this->authenticateViaGuard($guardAuthenticator, $token, $this->providerKey); @@ -109,7 +103,7 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface public function supports(TokenInterface $token) { - if ($token instanceof PreAuthenticationToken) { + if ($token instanceof PreAuthenticationGuardToken) { return null !== $this->findOriginatingAuthenticator($token); } @@ -121,12 +115,7 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface $this->rememberMeServices = $rememberMeServices; } - protected function getAuthenticatorKey(string $key): string - { - return $this->providerKey.'_'.$key; - } - - private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, \Symfony\Component\Security\Http\Authenticator\Token\PreAuthenticationToken $token, string $providerKey): TokenInterface + private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, PreAuthenticationGuardToken $token, string $providerKey): TokenInterface { // get the user from the GuardAuthenticator $user = $guardAuthenticator->getUser($token->getCredentials(), $this->userProvider); @@ -160,4 +149,21 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface return $authenticatedToken; } + + private function findOriginatingAuthenticator(PreAuthenticationGuardToken $token): ?AuthenticatorInterface + { + // find the *one* Authenticator that this token originated from + foreach ($this->authenticators as $key => $authenticator) { + // get a key that's unique to *this* authenticator + // this MUST be the same as AuthenticatorManagerListener + $uniqueAuthenticatorKey = $this->providerKey.'_'.$key; + + if ($uniqueAuthenticatorKey === $token->getGuardProviderKey()) { + return $authenticator; + } + } + + // no matching authenticator found + return null; + } } diff --git a/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php b/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php index 6504aa1997..8c32d4b24f 100644 --- a/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php +++ b/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php @@ -18,7 +18,7 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Guard\AuthenticatorInterface; use Symfony\Component\Security\Guard\Firewall\GuardAuthenticationListener; -use Symfony\Component\Security\Guard\Token\PreAuthenticationToken; +use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; /** * @author Ryan Weaver @@ -53,7 +53,7 @@ class GuardAuthenticationListenerTest extends TestCase // a clone of the token that should be created internally $uniqueGuardKey = 'my_firewall_0'; - $nonAuthedToken = new PreAuthenticationToken($credentials, $uniqueGuardKey); + $nonAuthedToken = new PreAuthenticationGuardToken($credentials, $uniqueGuardKey); $this->authenticationManager ->expects($this->once()) @@ -267,7 +267,7 @@ class GuardAuthenticationListenerTest extends TestCase ->getMock(); $this->guardAuthenticatorHandler = $this->getMockBuilder( - 'Symfony\Component\Security\Guard\GuardHandler' + 'Symfony\Component\Security\Guard\GuardAuthenticatorHandler' ) ->disableOriginalConstructor() ->getMock(); diff --git a/src/Symfony/Component/Security/Guard/Tests/GuardAuthenticatorHandlerTest.php b/src/Symfony/Component/Security/Guard/Tests/GuardAuthenticatorHandlerTest.php index d6dfacca10..e078a6be12 100644 --- a/src/Symfony/Component/Security/Guard/Tests/GuardAuthenticatorHandlerTest.php +++ b/src/Symfony/Component/Security/Guard/Tests/GuardAuthenticatorHandlerTest.php @@ -18,7 +18,7 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInt use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Guard\AuthenticatorInterface; -use Symfony\Component\Security\Guard\GuardHandler; +use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; use Symfony\Component\Security\Http\SecurityEvents; use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; @@ -47,7 +47,7 @@ class GuardAuthenticatorHandlerTest extends TestCase ->with($this->equalTo($loginEvent), $this->equalTo(SecurityEvents::INTERACTIVE_LOGIN)) ; - $handler = new GuardHandler($this->tokenStorage, $this->dispatcher); + $handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher); $handler->authenticateWithToken($this->token, $this->request); } @@ -60,7 +60,7 @@ class GuardAuthenticatorHandlerTest extends TestCase ->with($this->request, $this->token, $providerKey) ->willReturn($response); - $handler = new GuardHandler($this->tokenStorage, $this->dispatcher); + $handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher); $actualResponse = $handler->handleAuthenticationSuccess($this->token, $this->request, $this->guardAuthenticator, $providerKey); $this->assertSame($response, $actualResponse); } @@ -79,7 +79,7 @@ class GuardAuthenticatorHandlerTest extends TestCase ->with($this->request, $authException) ->willReturn($response); - $handler = new GuardHandler($this->tokenStorage, $this->dispatcher); + $handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher); $actualResponse = $handler->handleAuthenticationFailure($authException, $this->request, $this->guardAuthenticator, 'firewall_provider_key'); $this->assertSame($response, $actualResponse); } @@ -100,7 +100,7 @@ class GuardAuthenticatorHandlerTest extends TestCase ->with($this->request, $authException) ->willReturn($response); - $handler = new GuardHandler($this->tokenStorage, $this->dispatcher); + $handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher); $actualResponse = $handler->handleAuthenticationFailure($authException, $this->request, $this->guardAuthenticator, $actualProviderKey); $this->assertSame($response, $actualResponse); } @@ -124,7 +124,7 @@ class GuardAuthenticatorHandlerTest extends TestCase ->method('setToken') ->with($this->token); - $handler = new GuardHandler($this->tokenStorage, $this->dispatcher); + $handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher); $handler->authenticateWithToken($this->token, $this->request); } @@ -136,7 +136,7 @@ class GuardAuthenticatorHandlerTest extends TestCase ->method('onAuthentication') ->with($this->request, $this->token); - $handler = new GuardHandler($this->tokenStorage, $this->dispatcher); + $handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher); $handler->setSessionAuthenticationStrategy($this->sessionStrategy); $handler->authenticateWithToken($this->token, $this->request); } @@ -148,7 +148,7 @@ class GuardAuthenticatorHandlerTest extends TestCase $this->sessionStrategy->expects($this->never()) ->method('onAuthentication'); - $handler = new GuardHandler($this->tokenStorage, $this->dispatcher, ['some_provider_key']); + $handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher, ['some_provider_key']); $handler->setSessionAuthenticationStrategy($this->sessionStrategy); $handler->authenticateWithToken($this->token, $this->request, 'some_provider_key'); } diff --git a/src/Symfony/Component/Security/Guard/Tests/Provider/GuardAuthenticationProviderTest.php b/src/Symfony/Component/Security/Guard/Tests/Provider/GuardAuthenticationProviderTest.php index c1bb302f9c..477bf56622 100644 --- a/src/Symfony/Component/Security/Guard/Tests/Provider/GuardAuthenticationProviderTest.php +++ b/src/Symfony/Component/Security/Guard/Tests/Provider/GuardAuthenticationProviderTest.php @@ -18,7 +18,7 @@ use Symfony\Component\Security\Guard\AuthenticatorInterface; use Symfony\Component\Security\Guard\Provider\GuardAuthenticationProvider; use Symfony\Component\Security\Guard\Token\GuardTokenInterface; use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken; -use Symfony\Component\Security\Guard\Token\PreAuthenticationToken; +use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; /** * @author Ryan Weaver @@ -143,11 +143,11 @@ class GuardAuthenticationProviderTest extends TestCase $mockedUser = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock(); $provider = new GuardAuthenticationProvider($authenticators, $this->userProvider, 'first_firewall', $this->userChecker); - $token = new PreAuthenticationToken($mockedUser, 'first_firewall_1'); + $token = new PreAuthenticationGuardToken($mockedUser, 'first_firewall_1'); $supports = $provider->supports($token); $this->assertTrue($supports); - $token = new PreAuthenticationToken($mockedUser, 'second_firewall_0'); + $token = new PreAuthenticationGuardToken($mockedUser, 'second_firewall_0'); $supports = $provider->supports($token); $this->assertFalse($supports); } @@ -162,7 +162,7 @@ class GuardAuthenticationProviderTest extends TestCase $mockedUser = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock(); $provider = new GuardAuthenticationProvider($authenticators, $this->userProvider, 'first_firewall', $this->userChecker); - $token = new PreAuthenticationToken($mockedUser, 'second_firewall_0'); + $token = new PreAuthenticationGuardToken($mockedUser, 'second_firewall_0'); $provider->authenticate($token); } @@ -171,7 +171,7 @@ class GuardAuthenticationProviderTest extends TestCase $this->userProvider = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserProviderInterface')->getMock(); $this->userChecker = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserCheckerInterface')->getMock(); $this->preAuthenticationToken = $this->getMockBuilder( - 'Symfony\Component\Security\Guard\Token\PreAuthenticationToken' + 'Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken' ) ->disableOriginalConstructor() ->getMock(); diff --git a/src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php b/src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php new file mode 100644 index 0000000000..451d96c6ee --- /dev/null +++ b/src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Guard\Token; + +use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; + +/** + * The token used by the guard auth system before authentication. + * + * The GuardAuthenticationListener creates this, which is then consumed + * immediately by the GuardAuthenticationProvider. If authentication is + * successful, a different authenticated token is returned + * + * @author Ryan Weaver + */ +class PreAuthenticationGuardToken extends AbstractToken implements GuardTokenInterface +{ + private $credentials; + private $guardProviderKey; + + /** + * @param mixed $credentials + * @param string $guardProviderKey Unique key that bind this token to a specific AuthenticatorInterface + */ + public function __construct($credentials, string $guardProviderKey) + { + $this->credentials = $credentials; + $this->guardProviderKey = $guardProviderKey; + + parent::__construct([]); + + // never authenticated + parent::setAuthenticated(false); + } + + public function getGuardProviderKey() + { + return $this->guardProviderKey; + } + + /** + * Returns the user credentials, which might be an array of anything you + * wanted to put in there (e.g. username, password, favoriteColor). + * + * @return mixed The user credentials + */ + public function getCredentials() + { + return $this->credentials; + } + + public function setAuthenticated(bool $authenticated) + { + throw new \LogicException('The PreAuthenticationGuardToken is *never* authenticated.'); + } +} diff --git a/src/Symfony/Component/Security/Guard/Token/PreAuthenticationToken.php b/src/Symfony/Component/Security/Guard/Token/PreAuthenticationToken.php deleted file mode 100644 index 1ae9be445e..0000000000 --- a/src/Symfony/Component/Security/Guard/Token/PreAuthenticationToken.php +++ /dev/null @@ -1,29 +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\Guard\Token; - -/** - * The token used by the guard auth system before authentication. - * - * The GuardAuthenticationListener creates this, which is then consumed - * immediately by the GuardAuthenticationProvider. If authentication is - * successful, a different authenticated token is returned - * - * @author Ryan Weaver - */ -class PreAuthenticationToken extends \Symfony\Component\Security\Http\Authenticator\Token\CorePreAuthenticationGuardToken implements GuardTokenInterface -{ - public function getGuardKey() - { - return $this->getAuthenticatorKey(); - } -} diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php index 6a565ad1bb..f7dacacbc4 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php @@ -11,109 +11,206 @@ namespace Symfony\Component\Security\Http\Authentication; -use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\AuthenticationEvents; +use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; +use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\Token\PreAuthenticationToken; -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\Http\Event\InteractiveLoginEvent; +use Symfony\Component\Security\Http\Event\LoginFailureEvent; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +use Symfony\Component\Security\Http\SecurityEvents; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * @author Wouter de Jong - * @author Ryan Weaver + * @author Ryan Weaver + * @author Amaury Leroux de Lens * * @experimental in 5.1 */ -class AuthenticatorManager implements AuthenticationManagerInterface +class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthenticatorInterface { use AuthenticatorManagerTrait; private $authenticators; + private $tokenStorage; private $eventDispatcher; private $eraseCredentials; + private $logger; private $providerKey; /** - * @param AuthenticatorInterface[] $authenticators The authenticators, with keys that match what's passed to AuthenticatorManagerListener + * @param AuthenticatorInterface[] $authenticators The authenticators, with their unique providerKey as key */ - public function __construct(iterable $authenticators, EventDispatcherInterface $eventDispatcher, string $providerKey, bool $eraseCredentials = true) + public function __construct(iterable $authenticators, TokenStorageInterface $tokenStorage, EventDispatcherInterface $eventDispatcher, string $providerKey, ?LoggerInterface $logger = null, bool $eraseCredentials = true) { $this->authenticators = $authenticators; + $this->tokenStorage = $tokenStorage; $this->eventDispatcher = $eventDispatcher; $this->providerKey = $providerKey; + $this->logger = $logger; $this->eraseCredentials = $eraseCredentials; } - public function setEventDispatcher(EventDispatcherInterface $dispatcher) + public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request): ?Response { - $this->eventDispatcher = $dispatcher; + // create an authenticated token for the User + $token = $authenticator->createAuthenticatedToken($user, $this->providerKey); + // authenticate this in the system + $this->saveAuthenticatedToken($token, $request); + + // return the success metric + return $this->handleAuthenticationSuccess($token, $request, $authenticator); } - public function authenticate(TokenInterface $token) + public function supports(Request $request): ?bool { - if (!$token instanceof PreAuthenticationToken) { - /* - * The listener *only* passes PreAuthenticationToken instances. - * This means that an authenticated token (e.g. PostAuthenticationToken) - * 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. - */ + if (null !== $this->logger) { + $context = ['firewall_key' => $this->providerKey]; - // this should never happen - but technically, the token is - // authenticated... so it could just be returned - if ($token->isAuthenticated()) { - return $token; + if ($this->authenticators instanceof \Countable || \is_array($this->authenticators)) { + $context['authenticators'] = \count($this->authenticators); } - // this AccountStatusException causes the user to be logged out - throw new AuthenticationExpiredException(); + $this->logger->debug('Checking for guard authentication credentials.', $context); } - $authenticator = $this->findOriginatingAuthenticator($token); - if (null === $authenticator) { - $this->handleFailure(new ProviderNotFoundException(sprintf('Token with provider key "%s" did not originate from any of the authenticators.', $token->getAuthenticatorKey())), $token); + $authenticators = []; + $lazy = true; + foreach ($this->authenticators as $key => $authenticator) { + if (null !== $this->logger) { + $this->logger->debug('Checking support on authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); + } + + if (false !== $supports = $authenticator->supports($request)) { + $authenticators[$key] = $authenticator; + $lazy = $lazy && null === $supports; + } elseif (null !== $this->logger) { + $this->logger->debug('Authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); + } } + if (!$authenticators) { + return false; + } + + $request->attributes->set('_guard_authenticators', $authenticators); + + return $lazy ? null : true; + } + + public function authenticateRequest(Request $request): ?Response + { + $authenticators = $request->attributes->get('_guard_authenticators'); + $request->attributes->remove('_guard_authenticators'); + if (!$authenticators) { + return null; + } + + return $this->executeAuthenticators($authenticators, $request); + } + + /** + * @param AuthenticatorInterface[] $authenticators + */ + private function executeAuthenticators(array $authenticators, Request $request): ?Response + { + foreach ($authenticators as $key => $authenticator) { + // recheck if the authenticator still supports the listener. support() is called + // eagerly (before token storage is initialized), whereas authenticate() is called + // lazily (after initialization). This is important for e.g. the AnonymousAuthenticator + // as its support is relying on the (initialized) token in the TokenStorage. + if (false === $authenticator->supports($request)) { + $this->logger->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => \get_class($authenticator)]); + continue; + } + + $response = $this->executeAuthenticator($key, $authenticator, $request); + if (null !== $response) { + if (null !== $this->logger) { + $this->logger->debug('The "{authenticator}" authenticator set the response. Any later authenticator will not be called', ['authenticator' => \get_class($authenticator)]); + } + + return $response; + } + } + + return null; + } + + private function executeAuthenticator(string $uniqueAuthenticatorKey, AuthenticatorInterface $authenticator, Request $request): ?Response + { try { - $result = $this->authenticateViaAuthenticator($authenticator, $token, $token->getProviderKey()); - } catch (AuthenticationException $exception) { - $this->handleFailure($exception, $token); - } - - if (null !== $result) { - if (true === $this->eraseCredentials) { - $result->eraseCredentials(); + if (null !== $this->logger) { + $this->logger->debug('Calling getCredentials() on authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); } - if (null !== $this->eventDispatcher) { - $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($result), AuthenticationEvents::AUTHENTICATION_SUCCESS); + // allow the authenticator to fetch authentication info from the request + $credentials = $authenticator->getCredentials($request); + + if (null === $credentials) { + throw new \UnexpectedValueException(sprintf('The return value of "%1$s::getCredentials()" must not be null. Return false from "%1$s::supports()" instead.', \get_class($authenticator))); } + + if (null !== $this->logger) { + $this->logger->debug('Passing token information to the AuthenticatorManager', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); + } + + // authenticate the credentials (e.g. check password) + $token = $this->authenticateViaAuthenticator($authenticator, $credentials); + + if (null !== $this->logger) { + $this->logger->info('Authenticator successful!', ['token' => $token, 'authenticator' => \get_class($authenticator)]); + } + + // sets the token on the token storage, etc + $this->saveAuthenticatedToken($token, $request); + } catch (AuthenticationException $e) { + // oh no! Authentication failed! + + if (null !== $this->logger) { + $this->logger->info('Authenticator failed.', ['exception' => $e, 'authenticator' => \get_class($authenticator)]); + } + + $response = $this->handleAuthenticationFailure($e, $request, $authenticator); + if ($response instanceof Response) { + return $response; + } + + return null; } - return $result; + // success! + $response = $this->handleAuthenticationSuccess($token, $request, $authenticator); + if ($response instanceof Response) { + if (null !== $this->logger) { + $this->logger->debug('Authenticator set success response.', ['response' => $response, 'authenticator' => \get_class($authenticator)]); + } + + return $response; + } + + if (null !== $this->logger) { + $this->logger->debug('Authenticator set no success response: request continues.', ['authenticator' => \get_class($authenticator)]); + } + + return null; } - protected function getAuthenticatorKey(string $key): string - { - // Authenticators in the AuthenticatorManager are already indexed - // by an unique key - return $key; - } - - private function authenticateViaAuthenticator(AuthenticatorInterface $authenticator, PreAuthenticationToken $token, string $providerKey): TokenInterface + private function authenticateViaAuthenticator(AuthenticatorInterface $authenticator, $credentials): TokenInterface { // get the user from the Authenticator - $user = $authenticator->getUser($token->getCredentials()); + $user = $authenticator->getUser($credentials); if (null === $user) { throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', \get_class($authenticator))); } @@ -122,22 +219,47 @@ class AuthenticatorManager implements AuthenticationManagerInterface throw new \UnexpectedValueException(sprintf('The %s::getUser() method must return a UserInterface. You returned %s.', \get_class($authenticator), \is_object($user) ? \get_class($user) : \gettype($user))); } - $event = new VerifyAuthenticatorCredentialsEvent($authenticator, $token, $user); + $event = new VerifyAuthenticatorCredentialsEvent($authenticator, $credentials, $user); $this->eventDispatcher->dispatch($event); if (true !== $event->areCredentialsValid()) { - throw new BadCredentialsException(sprintf('Authentication failed because %s did not approve the credentials.', \get_class($authenticator))); + throw new BadCredentialsException(sprintf('Authentication failed because "%s" did not approve the credentials.', \get_class($authenticator))); } - // turn the UserInterface into a TokenInterface - $authenticatedToken = $authenticator->createAuthenticatedToken($user, $providerKey); +// turn the UserInterface into a TokenInterface + $authenticatedToken = $authenticator->createAuthenticatedToken($user, $this->providerKey); if (!$authenticatedToken instanceof TokenInterface) { throw new \UnexpectedValueException(sprintf('The %s::createAuthenticatedToken() method must return a TokenInterface. You returned %s.', \get_class($authenticator), \is_object($authenticatedToken) ? \get_class($authenticatedToken) : \gettype($authenticatedToken))); } + if (true === $this->eraseCredentials) { + $authenticatedToken->eraseCredentials(); + } + + if (null !== $this->eventDispatcher) { + $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($authenticatedToken), AuthenticationEvents::AUTHENTICATION_SUCCESS); + } + return $authenticatedToken; } - private function handleFailure(AuthenticationException $exception, TokenInterface $token) + private function saveAuthenticatedToken(TokenInterface $authenticatedToken, Request $request) + { + $this->tokenStorage->setToken($authenticatedToken); + + $loginEvent = new InteractiveLoginEvent($request, $authenticatedToken); + $this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN); + } + + private function handleAuthenticationSuccess(TokenInterface $token, Request $request, AuthenticatorInterface $authenticator): ?Response + { + $response = $authenticator->onAuthenticationSuccess($request, $token, $this->providerKey); + + $this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $token, $request, $response, $this->providerKey)); + + return $loginSuccessEvent->getResponse(); + } + + private function handleAuthenticationFailure(AuthenticationException $exception, TokenInterface $token) { if (null !== $this->eventDispatcher) { $this->eventDispatcher->dispatch(new AuthenticationFailureEvent($token, $exception), AuthenticationEvents::AUTHENTICATION_FAILURE); @@ -147,4 +269,17 @@ class AuthenticatorManager implements AuthenticationManagerInterface throw $exception; } + + /** + * Handles an authentication failure and returns the Response for the authenticator. + */ + private function handleAuthenticatorFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator): ?Response + { + $response = $authenticator->onAuthenticationFailure($request, $authenticationException); + + $this->eventDispatcher->dispatch($loginFailureEvent = new LoginFailureEvent($authenticationException, $authenticator, $request, $response, $this->providerKey)); + + // returning null is ok, it means they want the request to continue + return $loginFailureEvent->getResponse(); + } } diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerInterface.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerInterface.php new file mode 100644 index 0000000000..89bcef8b52 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerInterface.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authentication; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Http\Firewall\AbstractListener; + +/** + * @author Wouter de Jong + * @author Ryan Weaver + * + * @experimental in Symfony 5.1 + */ +interface AuthenticatorManagerInterface +{ + /** + * Called to see if authentication should be attempted on this request. + * + * @see AbstractListener::supports() + */ + public function supports(Request $request): ?bool; + + /** + * Tries to authenticate the request and returns a response - if any authenticator set one. + */ + public function authenticateRequest(Request $request): ?Response; +} diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerTrait.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerTrait.php deleted file mode 100644 index b1df45daab..0000000000 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerTrait.php +++ /dev/null @@ -1,46 +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\Http\Authentication; - -use Symfony\Component\Security\Guard\AuthenticatorInterface as GuardAuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface as CoreAuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\Token\PreAuthenticationToken; - -/** - * @author Ryan Weaver - * - * @internal - */ -trait AuthenticatorManagerTrait -{ - /** - * @return CoreAuthenticatorInterface|GuardAuthenticatorInterface|null - */ - private function findOriginatingAuthenticator(PreAuthenticationToken $token) - { - // find the *one* Authenticator that this token originated from - foreach ($this->authenticators as $key => $authenticator) { - // get a key that's unique to *this* authenticator - // this MUST be the same as AuthenticatorManagerListener - $uniqueAuthenticatorKey = $this->getAuthenticatorKey($key); - - if ($uniqueAuthenticatorKey === $token->getAuthenticatorKey()) { - return $authenticator; - } - } - - // no matching authenticator found - return null; - } - - abstract protected function getAuthenticatorKey(string $key): string; -} diff --git a/src/Symfony/Component/Security/Http/Authentication/NoopAuthenticationManager.php b/src/Symfony/Component/Security/Http/Authentication/NoopAuthenticationManager.php new file mode 100644 index 0000000000..1a6efeb379 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authentication/NoopAuthenticationManager.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authentication; + +use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * This class is used when the authenticator system is activated. + * + * This is used to not break AuthenticationChecker and ContextListener when + * using the authenticator system. Once the authenticator system is no longer + * experimental, this class can be used trigger deprecation notices. + * + * @internal + * + * @author Wouter de Jong + */ +class NoopAuthenticationManager implements AuthenticationManagerInterface +{ + public function authenticate(TokenInterface $token) + { + } +} diff --git a/src/Symfony/Component/Security/Http/Authentication/UserAuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authentication/UserAuthenticatorInterface.php new file mode 100644 index 0000000000..76cb572921 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authentication/UserAuthenticatorInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authentication; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; + +/** + * @author Wouter de Jong + * + * @experimental in Symfony 5.1 + */ +interface UserAuthenticatorInterface +{ + /** + * Convenience method to manually login a user and return a + * Response *if any* for success. + */ + public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request): ?Response; +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php index 0301a97110..3683827d12 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php @@ -18,7 +18,7 @@ use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken; /** * An optional base class that creates the necessary tokens for you. * - * @author Ryan Weaver + * @author Ryan Weaver * * @experimental in 5.1 */ diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php index 3469e8c509..e702144787 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php @@ -21,7 +21,7 @@ use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface /** * A base class to make form login authentication easier! * - * @author Ryan Weaver + * @author Ryan Weaver * * @experimental in 5.1 */ diff --git a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php index 6a85062e6c..0f1053e109 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php @@ -20,7 +20,7 @@ use Symfony\Component\Security\Core\User\UserInterface; /** * The interface for all authenticators. * - * @author Ryan Weaver + * @author Ryan Weaver * @author Amaury Leroux de Lens * @author Wouter de Jong * @@ -32,6 +32,8 @@ interface AuthenticatorInterface * Does the authenticator support the given Request? * * If this returns false, the authenticator will be skipped. + * + * Returning null means authenticate() can be called lazily when accessing the token storage. */ public function supports(Request $request): ?bool; diff --git a/src/Symfony/Component/Security/Http/Authenticator/Token/PreAuthenticationToken.php b/src/Symfony/Component/Security/Http/Authenticator/Token/PreAuthenticationToken.php deleted file mode 100644 index 27daf7f8ba..0000000000 --- a/src/Symfony/Component/Security/Http/Authenticator/Token/PreAuthenticationToken.php +++ /dev/null @@ -1,73 +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\Http\Authenticator\Token; - -use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; - -/** - * The token used by the authenticator system before authentication. - * - * The AuthenticatorManagerListener creates this, which is then consumed - * immediately by the AuthenticatorManager. If authentication is - * successful, a different authenticated token is returned - * - * @author Ryan Weaver - */ -class PreAuthenticationToken extends AbstractToken -{ - private $credentials; - private $authenticatorProviderKey; - private $providerKey; - - /** - * @param mixed $credentials - * @param string $authenticatorProviderKey Unique key that bind this token to a specific AuthenticatorInterface - * @param string|null $providerKey The general provider key (when using with HTTP, this is the firewall name) - */ - public function __construct($credentials, string $authenticatorProviderKey, ?string $providerKey = null) - { - $this->credentials = $credentials; - $this->authenticatorProviderKey = $authenticatorProviderKey; - $this->providerKey = $providerKey; - - parent::__construct([]); - - // never authenticated - parent::setAuthenticated(false); - } - - public function getProviderKey(): ?string - { - return $this->providerKey; - } - - public function getAuthenticatorKey() - { - return $this->authenticatorProviderKey; - } - - /** - * Returns the user credentials, which might be an array of anything you - * wanted to put in there (e.g. username, password, favoriteColor). - * - * @return mixed The user credentials - */ - public function getCredentials() - { - return $this->credentials; - } - - public function setAuthenticated(bool $authenticated) - { - throw new \LogicException('The PreAuthenticationToken is *never* authenticated.'); - } -} diff --git a/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php b/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php index bc4e551e91..03a1c7a78c 100644 --- a/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php +++ b/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php @@ -53,6 +53,11 @@ class LoginFailureEvent extends Event return $this->request; } + public function setResponse(?Response $response) + { + $this->response = $response; + } + public function getResponse(): ?Response { return $this->response; diff --git a/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php index 22e11a8c87..6e48e171b6 100644 --- a/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php +++ b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php @@ -50,13 +50,18 @@ class LoginSuccessEvent extends Event return $this->request; } - public function getResponse(): ?Response - { - return $this->response; - } - public function getProviderKey(): string { return $this->providerKey; } + + public function setResponse(?Response $response): void + { + $this->response = $response; + } + + public function getResponse(): ?Response + { + return $this->response; + } } diff --git a/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php b/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php index 87bcb56a8b..cc37bf33f2 100644 --- a/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php +++ b/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php @@ -19,14 +19,14 @@ use Symfony\Contracts\EventDispatcher\Event; class VerifyAuthenticatorCredentialsEvent extends Event { private $authenticator; - private $preAuthenticatedToken; private $user; + private $credentials; private $credentialsValid = false; - public function __construct(AuthenticatorInterface $authenticator, TokenInterface $preAuthenticatedToken, ?UserInterface $user) + public function __construct(AuthenticatorInterface $authenticator, $credentials, ?UserInterface $user) { $this->authenticator = $authenticator; - $this->preAuthenticatedToken = $preAuthenticatedToken; + $this->credentials = $credentials; $this->user = $user; } @@ -35,9 +35,9 @@ class VerifyAuthenticatorCredentialsEvent extends Event return $this->authenticator; } - public function getPreAuthenticatedToken(): TokenInterface + public function getCredentials() { - return $this->preAuthenticatedToken; + return $this->credentials; } public function getUser(): ?UserInterface diff --git a/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php b/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php index 086eb92431..6795100a9c 100644 --- a/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php @@ -41,7 +41,7 @@ class AuthenticatingListener implements EventSubscriberInterface $user = $event->getUser(); $event->setCredentialsValid($this->encoderFactory->getEncoder($user)->isPasswordValid( $user->getPassword(), - $authenticator->getPassword($event->getPreAuthenticatedToken()->getCredentials()), + $authenticator->getPassword($event->getCredentials()), $user->getSalt() )); @@ -58,7 +58,7 @@ class AuthenticatingListener implements EventSubscriberInterface } if ($authenticator instanceof CustomAuthenticatedInterface) { - $event->setCredentialsValid($authenticator->checkCredentials($event->getPreAuthenticatedToken()->getCredentials(), $event->getUser())); + $event->setCredentialsValid($authenticator->checkCredentials($event->getCredentials(), $event->getUser())); return; } diff --git a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php index b57605e551..c97b722ff1 100644 --- a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php @@ -36,12 +36,11 @@ class PasswordMigratingListener implements EventSubscriberInterface return; } - $token = $event->getPreAuthenticatedToken(); - if (null !== $password = $authenticator->getPassword($token->getCredentials())) { + if (null !== $password = $authenticator->getPassword($event->getCredentials())) { return; } - $user = $token->getUser(); + $user = $event->getUser(); if (!$user instanceof UserInterface) { return; } diff --git a/src/Symfony/Component/Security/Http/EventListener/SessionStrategyListener.php b/src/Symfony/Component/Security/Http/EventListener/SessionStrategyListener.php new file mode 100644 index 0000000000..436d525a5a --- /dev/null +++ b/src/Symfony/Component/Security/Http/EventListener/SessionStrategyListener.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\Http\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; +use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy; +use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; + +/** + * Migrates/invalidate the session after successful login. + * + * This should be registered as subscriber to any "stateful" firewalls. + * + * @see SessionAuthenticationStrategy + * + * @author Wouter de Jong + */ +class SessionStrategyListener implements EventSubscriberInterface +{ + private $sessionAuthenticationStrategy; + private $statelessProviderKeys; + + public function __construct(SessionAuthenticationStrategyInterface $sessionAuthenticationStrategy, array $statelessProviderKeys = []) + { + $this->sessionAuthenticationStrategy = $sessionAuthenticationStrategy; + $this->statelessProviderKeys = $statelessProviderKeys; + } + + public function onSuccessfulLogin(LoginSuccessEvent $event): void + { + $request = $event->getRequest(); + $token = $event->getAuthenticatedToken(); + $providerKey = $event->getProviderKey(); + + if (!$request->hasSession() || !$request->hasPreviousSession() || \in_array($providerKey, $this->statelessProviderKeys, true)) { + return; + } + + $this->sessionAuthenticationStrategy->onAuthentication($request, $token); + } + + public static function getSubscribedEvents(): array + { + return [LoginSuccessEvent::class => 'onSuccessfulLogin']; + } +} diff --git a/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php b/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php index 016bb826af..f30d9b6004 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php @@ -11,192 +11,39 @@ namespace Symfony\Component\Security\Http\Firewall; -use Psr\Log\LoggerInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\RequestEvent; -use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; -use Symfony\Component\Security\Core\Exception\AuthenticationException; -use Symfony\Component\Security\Http\Authentication\AuthenticatorHandler; -use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\Token\PreAuthenticationToken; -use Symfony\Component\Security\Http\Event\LoginFailureEvent; -use Symfony\Component\Security\Http\Event\LoginSuccessEvent; +use Symfony\Component\Security\Http\Authentication\AuthenticatorManagerInterface; /** + * Firewall authentication listener that delegates to the authenticator system. + * * @author Wouter de Jong - * @author Ryan Weaver - * @author Amaury Leroux de Lens * * @experimental in 5.1 */ class AuthenticatorManagerListener extends AbstractListener { private $authenticatorManager; - private $authenticatorHandler; - private $authenticators; - protected $providerKey; - private $eventDispatcher; - protected $logger; - /** - * @param AuthenticatorInterface[] $authenticators - */ - public function __construct(AuthenticationManagerInterface $authenticationManager, AuthenticatorHandler $authenticatorHandler, iterable $authenticators, string $providerKey, EventDispatcherInterface $eventDispatcher, ?LoggerInterface $logger = null) + public function __construct(AuthenticatorManagerInterface $authenticationManager) { $this->authenticatorManager = $authenticationManager; - $this->authenticatorHandler = $authenticatorHandler; - $this->authenticators = $authenticators; - $this->providerKey = $providerKey; - $this->logger = $logger; - $this->eventDispatcher = $eventDispatcher; } public function supports(Request $request): ?bool { - if (null !== $this->logger) { - $context = ['firewall_key' => $this->providerKey]; - - if ($this->authenticators instanceof \Countable || \is_array($this->authenticators)) { - $context['authenticators'] = \count($this->authenticators); - } - - $this->logger->debug('Checking for guard authentication credentials.', $context); - } - - [$authenticators, $lazy] = $this->getSupportingAuthenticators($request); - if (!$authenticators) { - return false; - } - - $request->attributes->set('_guard_authenticators', $authenticators); - - return $lazy ? null : true; + return $this->authenticatorManager->supports($request); } - public function authenticate(RequestEvent $event) + public function authenticate(RequestEvent $event): void { $request = $event->getRequest(); - $authenticators = $request->attributes->get('_guard_authenticators'); - $request->attributes->remove('_guard_authenticators'); - if (!$authenticators) { + $response = $this->authenticatorManager->authenticateRequest($request); + if (null === $response) { return; } - $this->executeAuthenticators($authenticators, $event); - } - - protected function getSupportingAuthenticators(Request $request): array - { - $authenticators = []; - $lazy = true; - foreach ($this->authenticators as $key => $authenticator) { - if (null !== $this->logger) { - $this->logger->debug('Checking support on authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); - } - - if (false !== $supports = $authenticator->supports($request)) { - $authenticators[$key] = $authenticator; - $lazy = $lazy && null === $supports; - } elseif (null !== $this->logger) { - $this->logger->debug('Authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); - } - } - - return [$authenticators, $lazy]; - } - - /** - * @param AuthenticatorInterface[] $authenticators - */ - protected function executeAuthenticators(array $authenticators, RequestEvent $event): void - { - foreach ($authenticators as $key => $authenticator) { - // recheck if the authenticator still supports the listener. support() is called - // eagerly (before token storage is initialized), whereas authenticate() is called - // lazily (after initialization). This is important for e.g. the AnonymousAuthenticator - // as its support is relying on the (initialized) token in the TokenStorage. - if (false === $authenticator->supports($event->getRequest())) { - $this->logger->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => \get_class($authenticator)]); - continue; - } - - $this->executeAuthenticator($key, $authenticator, $event); - - if ($event->hasResponse()) { - if (null !== $this->logger) { - $this->logger->debug('The "{authenticator}" authenticator set the response. Any later authenticator will not be called', ['authenticator' => \get_class($authenticator)]); - } - - break; - } - } - } - - private function executeAuthenticator(string $uniqueAuthenticatorKey, AuthenticatorInterface $authenticator, RequestEvent $event): void - { - $request = $event->getRequest(); - try { - if (null !== $this->logger) { - $this->logger->debug('Calling getCredentials() on authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); - } - - // allow the authenticator to fetch authentication info from the request - $credentials = $authenticator->getCredentials($request); - - if (null === $credentials) { - throw new \UnexpectedValueException(sprintf('The return value of "%1$s::getCredentials()" must not be null. Return false from "%1$s::supports()" instead.', \get_class($authenticator))); - } - - // create a token with the unique key, so that the provider knows which authenticator to use - $token = new PreAuthenticationToken($credentials, $uniqueAuthenticatorKey, $uniqueAuthenticatorKey); - - if (null !== $this->logger) { - $this->logger->debug('Passing token information to the AuthenticatorManager', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); - } - // pass the token into the AuthenticationManager system - // this indirectly calls AuthenticatorManager::authenticate() - $token = $this->authenticatorManager->authenticate($token); - - if (null !== $this->logger) { - $this->logger->info('Authenticator successful!', ['token' => $token, 'authenticator' => \get_class($authenticator)]); - } - - // sets the token on the token storage, etc - $this->authenticatorHandler->authenticateWithToken($token, $request, $this->providerKey); - } catch (AuthenticationException $e) { - // oh no! Authentication failed! - - if (null !== $this->logger) { - $this->logger->info('Authenticator failed.', ['exception' => $e, 'authenticator' => \get_class($authenticator)]); - } - - $response = $this->authenticatorHandler->handleAuthenticationFailure($e, $request, $authenticator, $this->providerKey); - - if ($response instanceof Response) { - $event->setResponse($response); - } - - $this->eventDispatcher->dispatch(new LoginFailureEvent($e, $authenticator, $request, $response, $this->providerKey)); - - return; - } - - // success! - $response = $this->authenticatorHandler->handleAuthenticationSuccess($token, $request, $authenticator, $this->providerKey); - if ($response instanceof Response) { - if (null !== $this->logger) { - $this->logger->debug('Authenticator set success response.', ['response' => $response, 'authenticator' => \get_class($authenticator)]); - } - - $event->setResponse($response); - } else { - if (null !== $this->logger) { - $this->logger->debug('Authenticator set no success response: request continues.', ['authenticator' => \get_class($authenticator)]); - } - } - - $this->eventDispatcher->dispatch(new LoginSuccessEvent($authenticator, $token, $request, $response, $this->providerKey)); + $event->setResponse($response); } }