From ddf430fc1ef75724bba87670310b3cb79f2daffe Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Wed, 12 Feb 2020 23:56:17 +0100 Subject: [PATCH] Added remember me functionality --- .../Security/Factory/AnonymousFactory.php | 2 +- .../Factory/AuthenticatorFactoryInterface.php | 2 +- .../Security/Factory/FormLoginFactory.php | 2 +- .../Security/Factory/HttpBasicFactory.php | 2 +- .../Security/Factory/RememberMeFactory.php | 133 +++++++++++++----- .../DependencyInjection/SecurityExtension.php | 36 +++-- .../Resources/config/authenticators.xml | 13 +- .../AbstractLoginFormAuthenticator.php | 12 +- .../Authenticator/AnonymousAuthenticator.php | 5 - .../Authenticator/AuthenticatorInterface.php | 14 -- .../Authenticator/HttpBasicAuthenticator.php | 5 - .../Authenticator/RememberMeAuthenticator.php | 110 +++++++++++++++ .../RememberMeAuthenticatorInterface.php | 31 ++++ .../Http/EventListener/RememberMeListener.php | 29 ++-- .../RememberMe/AbstractRememberMeServices.php | 5 + 15 files changed, 296 insertions(+), 105 deletions(-) create mode 100644 src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticatorInterface.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php index b7e2347a577a..cf77d99fdf0b 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php @@ -42,7 +42,7 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider, return [$providerId, $listenerId, $defaultEntryPoint]; } - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string { if (null === $config['secret']) { $config['secret'] = new Parameter('container.build_hash'); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php index e85ba0b495f7..acd1fce318e9 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php @@ -25,5 +25,5 @@ interface AuthenticatorFactoryInterface * * @return string|string[] The authenticator service ID(s) to be used by the firewall */ - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, ?string $userProviderId); + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId); } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 368cde156e7f..555cac383ed8 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -97,7 +97,7 @@ public function createEntryPoint(ContainerBuilder $container, string $id, array return $entryPointId; } - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string { $authenticatorId = 'security.authenticator.form_login.'.$id; $defaultOptions = array_merge($this->defaultSuccessHandlerOptions, $this->options); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php index dea437e94c38..9d121b17fec4 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php @@ -46,7 +46,7 @@ public function create(ContainerBuilder $container, string $id, array $config, s return [$provider, $listenerId, $entryPointId]; } - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string { $authenticatorId = 'security.authenticator.http_basic.'.$id; $container diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php index 06ad4134bd1e..979acc79dc26 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php @@ -20,7 +20,7 @@ use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\Security\Http\EventListener\RememberMeLogoutListener; -class RememberMeFactory implements SecurityFactoryInterface +class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface { protected $options = [ 'name' => 'REMEMBERME', @@ -46,29 +46,8 @@ public function create(ContainerBuilder $container, string $id, array $config, ? ; // remember me services - if (isset($config['service'])) { - $templateId = $config['service']; - $rememberMeServicesId = $templateId.'.'.$id; - } elseif (isset($config['token_provider'])) { - $templateId = 'security.authentication.rememberme.services.persistent'; - $rememberMeServicesId = $templateId.'.'.$id; - } else { - $templateId = 'security.authentication.rememberme.services.simplehash'; - $rememberMeServicesId = $templateId.'.'.$id; - } - - $rememberMeServices = $container->setDefinition($rememberMeServicesId, new ChildDefinition($templateId)); - $rememberMeServices->replaceArgument(1, $config['secret']); - $rememberMeServices->replaceArgument(2, $id); - - if (isset($config['token_provider'])) { - $rememberMeServices->addMethodCall('setTokenProvider', [ - new Reference($config['token_provider']), - ]); - } - - // remember-me options - $rememberMeServices->replaceArgument(3, array_intersect_key($config, $this->options)); + $templateId = $this->generateRememberMeServicesTemplateId($config, $id); + $rememberMeServicesId = $templateId.'.'.$id; // attach to remember-me aware listeners $userProviders = []; @@ -93,17 +72,8 @@ public function create(ContainerBuilder $container, string $id, array $config, ? ; } } - if ($config['user_providers']) { - $userProviders = []; - foreach ($config['user_providers'] as $providerName) { - $userProviders[] = new Reference('security.user.provider.concrete.'.$providerName); - } - } - if (0 === \count($userProviders)) { - throw new \RuntimeException('You must configure at least one remember-me aware listener (such as form-login) for each firewall that has remember-me enabled.'); - } - $rememberMeServices->replaceArgument(0, new IteratorArgument(array_unique($userProviders))); + $this->createRememberMeServices($container, $id, $templateId, $userProviders, $config); // remember-me listener $listenerId = 'security.authentication.listener.rememberme.'.$id; @@ -119,6 +89,42 @@ public function create(ContainerBuilder $container, string $id, array $config, ? return [$authProviderId, $listenerId, $defaultEntryPoint]; } + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string + { + $templateId = $this->generateRememberMeServicesTemplateId($config, $id); + $rememberMeServicesId = $templateId.'.'.$id; + + // create remember me services (which manage the remember me cookies) + $this->createRememberMeServices($container, $id, $templateId, [new Reference($userProviderId)], $config); + + // create remember me listener (which executes the remember me services for other authenticators and logout) + $this->createRememberMeListener($container, $id, $rememberMeServicesId); + + // create remember me authenticator (which re-authenticates the user based on the remember me cookie) + $authenticatorId = 'security.authenticator.remember_me.'.$id; + $container + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.remember_me')) + ->replaceArgument(0, new Reference($rememberMeServicesId)) + ->replaceArgument(3, array_intersect_key($config, $this->options)) + ; + + foreach ($container->findTaggedServiceIds('security.remember_me_aware') as $serviceId => $attributes) { + // register ContextListener + if ('security.context_listener' === substr($serviceId, 0, 25)) { + $container + ->getDefinition($serviceId) + ->addMethodCall('setRememberMeServices', [new Reference($rememberMeServicesId)]) + ; + + continue; + } + + throw new \LogicException(sprintf('Symfony Authenticator Security dropped support for the "security.remember_me_aware" tag, service "%s" will no longer work as expected.', $serviceId)); + } + + return $authenticatorId; + } + public function getPosition() { return 'remember_me'; @@ -163,4 +169,63 @@ public function addConfiguration(NodeDefinition $node) } } } + + private function generateRememberMeServicesTemplateId(array $config, string $id): string + { + if (isset($config['service'])) { + return $config['service']; + } + + if (isset($config['token_provider'])) { + return 'security.authentication.rememberme.services.persistent'; + } + + return 'security.authentication.rememberme.services.simplehash'; + } + + private function createRememberMeServices(ContainerBuilder $container, string $id, string $templateId, array $userProviders, array $config): void + { + $rememberMeServicesId = $templateId.'.'.$id; + + $rememberMeServices = $container->setDefinition($rememberMeServicesId, new ChildDefinition($templateId)); + $rememberMeServices->replaceArgument(1, $config['secret']); + $rememberMeServices->replaceArgument(2, $id); + + if (isset($config['token_provider'])) { + $rememberMeServices->addMethodCall('setTokenProvider', [ + new Reference($config['token_provider']), + ]); + } + + // remember-me options + $rememberMeServices->replaceArgument(3, array_intersect_key($config, $this->options)); + + if ($config['user_providers']) { + $userProviders = []; + foreach ($config['user_providers'] as $providerName) { + $userProviders[] = new Reference('security.user.provider.concrete.'.$providerName); + } + } + + if (0 === \count($userProviders)) { + throw new \RuntimeException('You must configure at least one remember-me aware listener (such as form-login) for each firewall that has remember-me enabled.'); + } + + $rememberMeServices->replaceArgument(0, new IteratorArgument(array_unique($userProviders))); + } + + private function createRememberMeListener(ContainerBuilder $container, string $id, string $rememberMeServicesId): void + { + $container + ->setDefinition('security.listener.remember_me.'.$id, new ChildDefinition('security.listener.remember_me')) + ->addTag('kernel.event_subscriber') + ->replaceArgument(0, new Reference($rememberMeServicesId)) + ->replaceArgument(1, $id) + ; + + $container + ->setDefinition('security.logout.listener.remember_me.'.$id, new Definition(RememberMeLogoutListener::class)) + ->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$id]) + ->addArgument(new Reference($rememberMeServicesId)); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 0e857e53d113..97ede2281fa3 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -26,6 +26,7 @@ use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; @@ -34,6 +35,7 @@ use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; +use Symfony\Component\Security\Core\User\ChainUserProvider; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Controller\UserValueResolver; use Twig\Extension\AbstractExtension; @@ -230,9 +232,16 @@ private function createFirewalls(array $config, ContainerBuilder $container) foreach ($providerIds as $userProviderId) { $userProviders[] = new Reference($userProviderId); } - $arguments[1] = new IteratorArgument($userProviders); + $arguments[1] = $userProviderIteratorsArgument = new IteratorArgument($userProviders); $contextListenerDefinition->setArguments($arguments); + if (\count($userProviders) > 1) { + $container->setDefinition('security.user_providers', new Definition(ChainUserProvider::class, [$userProviderIteratorsArgument])) + ->setPublic(false); + } else { + $container->setAlias('security.user_providers', new Alias(current($providerIds)))->setPublic(false); + } + if (1 === \count($providerIds)) { $container->setAlias(UserProviderInterface::class, current($providerIds)); } @@ -423,16 +432,6 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ // Determine default entry point $configuredEntryPoint = isset($firewall['entry_point']) ? $firewall['entry_point'] : null; - if ($this->authenticatorManagerEnabled) { - // Remember me listener (must be before calling createAuthenticationListeners() to inject remember me services) - $container - ->setDefinition('security.listener.remember_me.'.$id, new ChildDefinition('security.listener.remember_me')) - ->replaceArgument(0, $id) - ->addTag('kernel.event_subscriber') - ->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']) - ; - } - // Authentication listeners $firewallAuthenticationProviders = []; list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $firewallAuthenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint, $contextListenerId); @@ -554,7 +553,7 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri return [$listeners, $defaultEntryPoint]; } - private function getUserProvider(ContainerBuilder $container, string $id, array $firewall, string $factoryKey, ?string $defaultProvider, array $providerIds, ?string $contextListenerId): ?string + private function getUserProvider(ContainerBuilder $container, string $id, array $firewall, string $factoryKey, ?string $defaultProvider, array $providerIds, ?string $contextListenerId): string { if (isset($firewall[$factoryKey]['provider'])) { if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall[$factoryKey]['provider'])])) { @@ -564,13 +563,8 @@ private function getUserProvider(ContainerBuilder $container, string $id, array return $providerIds[$normalizedName]; } - if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey) { - if ('remember_me' === $factoryKey && $contextListenerId) { - $container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']); - } - - // RememberMeFactory will use the firewall secret when created - return null; + if ('remember_me' === $factoryKey && $contextListenerId) { + $container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']); } if ($defaultProvider) { @@ -587,6 +581,10 @@ private function getUserProvider(ContainerBuilder $container, string $id, array return $userProvider; } + if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey) { + return 'security.user_providers'; + } + throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "%s" listener on "%s" firewall is ambiguous as there is more than one registered provider.', $factoryKey, $id)); } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml index b42cf0fab02c..9ec5f17e0a20 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml @@ -52,7 +52,8 @@ class="Symfony\Component\Security\Http\EventListener\RememberMeListener" abstract="true"> - + remember me services + provider key @@ -82,5 +83,15 @@ secret + + + remember me services + %kernel.secret% + + options + + diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php index 07c71b1c3b41..3469e8c50991 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php @@ -25,7 +25,7 @@ * * @experimental in 5.1 */ -abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface +abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, RememberMeAuthenticatorInterface { /** * Return the URL to the login page. @@ -46,11 +46,6 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio return new RedirectResponse($url); } - public function supportsRememberMe(): bool - { - return true; - } - /** * Override to control what happens when the user hits a secure page * but isn't logged in yet. @@ -61,4 +56,9 @@ public function start(Request $request, AuthenticationException $authException = return new RedirectResponse($url); } + + public function supportsRememberMe(): bool + { + return true; + } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php index 7e56b715797c..93d69312182c 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php @@ -75,9 +75,4 @@ public function onAuthenticationSuccess(Request $request, TokenInterface $token, { return null; } - - public function supportsRememberMe(): bool - { - return false; - } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php index 5530eb32dddd..6a85062e6c1b 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php @@ -102,18 +102,4 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio * will be authenticated. This makes sense, for example, with an API. */ public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response; - - /** - * Does this method support remember me cookies? - * - * Remember me cookie will be set if *all* of the following are met: - * A) This method returns true - * B) The remember_me key under your firewall is configured - * C) The "remember me" functionality is activated. This is usually - * done by having a _remember_me checkbox in your form, but - * can be configured by the "always_remember_me" and "remember_me_parameter" - * parameters under the "remember_me" firewall key - * D) The onAuthenticationSuccess method returns a Response object - */ - public function supportsRememberMe(): bool; } diff --git a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php index 51ad3339b796..f896d924a802 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php @@ -94,9 +94,4 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio return $this->start($request, $exception); } - - public function supportsRememberMe(): bool - { - return false; - } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php new file mode 100644 index 000000000000..893bd099de70 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php @@ -0,0 +1,110 @@ + + * + * 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\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; +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\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices; +use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy; + +/** + * The RememberMe *Authenticator* performs remember me authentication. + * + * This authenticator is executed whenever a user's session + * expired and a remember me cookie was found. This authenticator + * then "re-authenticates" the user using the information in the + * cookie. + * + * @author Johannes M. Schmitt + * @author Wouter de Jong + * + * @final + */ +class RememberMeAuthenticator implements AuthenticatorInterface +{ + private $rememberMeServices; + private $secret; + private $tokenStorage; + private $options; + private $sessionStrategy; + + public function __construct(AbstractRememberMeServices $rememberMeServices, string $secret, TokenStorageInterface $tokenStorage, array $options, ?SessionAuthenticationStrategy $sessionStrategy = null) + { + $this->rememberMeServices = $rememberMeServices; + $this->secret = $secret; + $this->tokenStorage = $tokenStorage; + $this->options = $options; + $this->sessionStrategy = $sessionStrategy; + } + + public function supports(Request $request): ?bool + { + // do not overwrite already stored tokens (i.e. from the session) + if (null !== $this->tokenStorage->getToken()) { + return false; + } + + if (($cookie = $request->attributes->get(AbstractRememberMeServices::COOKIE_ATTR_NAME)) && null === $cookie->getValue()) { + return false; + } + + if (!$request->cookies->has($this->options['name'])) { + return false; + } + + // the `null` return value indicates that this authenticator supports lazy firewalls + return null; + } + + public function getCredentials(Request $request) + { + return [ + 'cookie_parts' => explode(AbstractRememberMeServices::COOKIE_DELIMITER, base64_decode($request->cookies->get($this->options['name']))), + 'request' => $request, + ]; + } + + /** + * @param array $credentials + */ + public function getUser($credentials): ?UserInterface + { + return $this->rememberMeServices->performLogin($credentials['cookie_parts'], $credentials['request']); + } + + public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface + { + return new RememberMeToken($user, $providerKey, $this->secret); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + $this->rememberMeServices->loginFail($request, $exception); + + return null; + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + { + if ($request->hasSession() && $request->getSession()->isStarted()) { + $this->sessionStrategy->onAuthentication($request, $token); + } + + return null; + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticatorInterface.php new file mode 100644 index 000000000000..d9eb6fa70bc8 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticatorInterface.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\Authenticator; + +/** + * This interface must be extended if the authenticator supports remember me functionality. + * + * Remember me cookie will be set if *all* of the following are met: + * A) SupportsRememberMe() returns true in the successful authenticator + * B) The remember_me key under your firewall is configured + * C) The "remember me" functionality is activated. This is usually + * done by having a _remember_me checkbox in your form, but + * can be configured by the "always_remember_me" and "remember_me_parameter" + * parameters under the "remember_me" firewall key + * D) The onAuthenticationSuccess method returns a Response object + * + * @author Wouter de Jong + */ +interface RememberMeAuthenticatorInterface +{ + public function supportsRememberMe(): bool; +} diff --git a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php index 882258b1a6a4..522f5090d64c 100644 --- a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php @@ -5,11 +5,19 @@ use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticatorInterface; use Symfony\Component\Security\Http\Event\LoginFailureEvent; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; /** + * The RememberMe *listener* creates and deletes remember me cookies. + * + * Upon login success or failure and support for remember me + * in the firewall and authenticator, this listener will create + * a remember me cookie. + * Upon login failure, all remember me cookies are removed. + * * @author Wouter de Jong * * @final @@ -17,23 +25,18 @@ */ class RememberMeListener implements EventSubscriberInterface { + private $rememberMeServices; private $providerKey; private $logger; - /** @var RememberMeServicesInterface|null */ - private $rememberMeServices; - public function __construct(string $providerKey, ?LoggerInterface $logger = null) + public function __construct(RememberMeServicesInterface $rememberMeServices, string $providerKey, ?LoggerInterface $logger = null) { + $this->rememberMeServices = $rememberMeServices; $this->providerKey = $providerKey; $this->logger = $logger; } - public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices): void - { - $this->rememberMeServices = $rememberMeServices; - } - public function onSuccessfulLogin(LoginSuccessEvent $event): void { if (!$this->isRememberMeEnabled($event->getAuthenticator(), $event->getProviderKey())) { @@ -59,15 +62,7 @@ private function isRememberMeEnabled(AuthenticatorInterface $authenticator, stri return false; } - if (null === $this->rememberMeServices) { - if (null !== $this->logger) { - $this->logger->debug('Remember me skipped: it is not configured for the firewall.', ['authenticator' => \get_class($authenticator)]); - } - - return false; - } - - if (!$authenticator->supportsRememberMe()) { + if (!$authenticator instanceof RememberMeAuthenticatorInterface || !$authenticator->supportsRememberMe()) { if (null !== $this->logger) { $this->logger->debug('Remember me skipped: your authenticator does not support it.', ['authenticator' => \get_class($authenticator)]); } diff --git a/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php b/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php index 22f9dde14b76..e9065d7f526f 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php +++ b/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php @@ -89,6 +89,11 @@ public function getSecret() return $this->secret; } + public function performLogin(array $cookieParts, Request $request): UserInterface + { + return $this->processAutoLoginCookie($cookieParts, $request); + } + /** * Implementation of RememberMeServicesInterface. Detects whether a remember-me * cookie was set, decodes it, and hands it to subclasses for further processing.