diff --git a/UPGRADE-5.1.md b/UPGRADE-5.1.md index 96c1c8b0e2d90..a1e1c84324b96 100644 --- a/UPGRADE-5.1.md +++ b/UPGRADE-5.1.md @@ -91,6 +91,8 @@ Security {% endif %} ``` + * Deprecated `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface`. Register a listener on the `LogoutEvent` event instead. + Yaml ---- diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfTokenClearingLogoutHandlerPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfTokenClearingLogoutHandlerPass.php index 0d7527c26bb79..2d6960e1fe45d 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfTokenClearingLogoutHandlerPass.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfTokenClearingLogoutHandlerPass.php @@ -14,6 +14,7 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Http\EventListener\CsrfTokenClearingLogoutListener; /** * @author Christian Flothmann @@ -33,10 +34,9 @@ public function process(ContainerBuilder $container) return; } - $container->register('security.logout.handler.csrf_token_clearing', 'Symfony\Component\Security\Http\Logout\CsrfTokenClearingLogoutHandler') + $container->register('security.logout.listener.csrf_token_clearing', CsrfTokenClearingLogoutListener::class) ->addArgument(new Reference('security.csrf.token_storage')) + ->addTag('kernel.event_subscriber') ->setPublic(false); - - $container->findDefinition('security.logout_listener')->addMethodCall('addHandler', [new Reference('security.logout.handler.csrf_token_clearing')]); } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 6361b0b4c36c8..c2251ad1f5445 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -16,6 +16,7 @@ use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; +use Symfony\Component\Security\Http\Event\LogoutEvent; use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy; /** @@ -205,7 +206,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->scalarNode('csrf_token_id')->defaultValue('logout')->end() ->scalarNode('path')->defaultValue('/logout')->end() ->scalarNode('target')->defaultValue('/')->end() - ->scalarNode('success_handler')->end() + ->scalarNode('success_handler')->setDeprecated(sprintf('The "%%node%%" at path "%%path%%" is deprecated, register a listener on the "%s" event instead.', LogoutEvent::class))->end() ->booleanNode('invalidate_session')->defaultTrue()->end() ->end() ->fixXmlConfig('delete_cookie') @@ -228,7 +229,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->fixXmlConfig('handler') ->children() ->arrayNode('handlers') - ->prototype('scalar')->end() + ->prototype('scalar')->setDeprecated(sprintf('The "%%node%%" at path "%%path%%" is deprecated, register a listener on the "%s" event instead.', LogoutEvent::class))->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php index a17f799b6c41e..ea1375b4232f7 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php @@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\Security\Http\EventListener\RememberMeLogoutListener; class RememberMeFactory implements SecurityFactoryInterface { @@ -55,13 +56,6 @@ public function create(ContainerBuilder $container, string $id, array $config, ? $rememberMeServicesId = $templateId.'.'.$id; } - if ($container->hasDefinition('security.logout_listener.'.$id)) { - $container - ->getDefinition('security.logout_listener.'.$id) - ->addMethodCall('addHandler', [new Reference($rememberMeServicesId)]) - ; - } - $rememberMeServices = $container->setDefinition($rememberMeServicesId, new ChildDefinition($templateId)); $rememberMeServices->replaceArgument(1, $config['secret']); $rememberMeServices->replaceArgument(2, $id); @@ -116,6 +110,11 @@ public function create(ContainerBuilder $container, string $id, array $config, ? $listener->replaceArgument(1, new Reference($rememberMeServicesId)); $listener->replaceArgument(5, $config['catch_exceptions']); + // remember-me logout listener + $container->register(RememberMeLogoutListener::class) + ->addArgument(new Reference($rememberMeServicesId)) + ->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$id]); + return [$authProviderId, $listenerId, $defaultEntryPoint]; } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 851f7da78690b..293e88856f620 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -14,6 +14,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface; +use Symfony\Bundle\SecurityBundle\Security\LegacyLogoutHandlerListener; use Symfony\Bundle\SecurityBundle\SecurityUserValueResolver; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\FileLocator; @@ -26,6 +27,7 @@ use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; @@ -307,6 +309,12 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $config->replaceArgument(5, $defaultProvider); + // Register Firewall-specific event dispatcher + $firewallEventDispatcherId = 'security.event_dispatcher.'.$id; + $container->register($firewallEventDispatcherId, EventDispatcher::class); + $container->setDefinition($firewallEventDispatcherId.'.event_bubbling_listener', new ChildDefinition('security.event_dispatcher.event_bubbling_listener')) + ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); + // Register listeners $listeners = []; $listenerKeys = []; @@ -334,44 +342,50 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ if (isset($firewall['logout'])) { $logoutListenerId = 'security.logout_listener.'.$id; $logoutListener = $container->setDefinition($logoutListenerId, new ChildDefinition('security.logout_listener')); + $logoutListener->replaceArgument(2, new Reference($firewallEventDispatcherId)); $logoutListener->replaceArgument(3, [ 'csrf_parameter' => $firewall['logout']['csrf_parameter'], 'csrf_token_id' => $firewall['logout']['csrf_token_id'], 'logout_path' => $firewall['logout']['path'], ]); - // add logout success handler + // add default logout listener if (isset($firewall['logout']['success_handler'])) { + // deprecated, to be removed in Symfony 6.0 $logoutSuccessHandlerId = $firewall['logout']['success_handler']; + $container->register('security.logout.listener.legacy_success_listener.'.$id, LegacyLogoutHandlerListener::class) + ->setArguments([new Reference($logoutSuccessHandlerId)]) + ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); } else { - $logoutSuccessHandlerId = 'security.logout.success_handler.'.$id; - $logoutSuccessHandler = $container->setDefinition($logoutSuccessHandlerId, new ChildDefinition('security.logout.success_handler')); - $logoutSuccessHandler->replaceArgument(1, $firewall['logout']['target']); + $logoutSuccessListenerId = 'security.logout.listener.default.'.$id; + $container->setDefinition($logoutSuccessListenerId, new ChildDefinition('security.logout.listener.default')) + ->replaceArgument(1, $firewall['logout']['target']) + ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); } - $logoutListener->replaceArgument(2, new Reference($logoutSuccessHandlerId)); // add CSRF provider if (isset($firewall['logout']['csrf_token_generator'])) { $logoutListener->addArgument(new Reference($firewall['logout']['csrf_token_generator'])); } - // add session logout handler + // add session logout listener if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) { - $logoutListener->addMethodCall('addHandler', [new Reference('security.logout.handler.session')]); + $container->setDefinition('security.logout.listener.session.'.$id, new ChildDefinition('security.logout.listener.session')) + ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); } - // add cookie logout handler + // add cookie logout listener if (\count($firewall['logout']['delete_cookies']) > 0) { - $cookieHandlerId = 'security.logout.handler.cookie_clearing.'.$id; - $cookieHandler = $container->setDefinition($cookieHandlerId, new ChildDefinition('security.logout.handler.cookie_clearing')); - $cookieHandler->addArgument($firewall['logout']['delete_cookies']); - - $logoutListener->addMethodCall('addHandler', [new Reference($cookieHandlerId)]); + $container->setDefinition('security.logout.listener.cookie_clearing.'.$id, new ChildDefinition('security.logout.listener.cookie_clearing')) + ->addArgument($firewall['logout']['delete_cookies']) + ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); } - // add custom handlers - foreach ($firewall['logout']['handlers'] as $handlerId) { - $logoutListener->addMethodCall('addHandler', [new Reference($handlerId)]); + // add custom listeners (deprecated) + foreach ($firewall['logout']['handlers'] as $i => $handlerId) { + $container->register('security.logout.listener.legacy_handler.'.$i, LegacyLogoutHandlerListener::class) + ->addArgument(new Reference($handlerId)) + ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); } // register with LogoutUrlGenerator diff --git a/src/Symfony/Bundle/SecurityBundle/EventListener/FirewallEventBubblingListener.php b/src/Symfony/Bundle/SecurityBundle/EventListener/FirewallEventBubblingListener.php new file mode 100644 index 0000000000000..c3415ccc8c84a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/EventListener/FirewallEventBubblingListener.php @@ -0,0 +1,44 @@ + + * + * 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 Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Http\Event\LogoutEvent; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +/** + * A listener that dispatches all security events from the firewall-specific + * dispatcher on the global event dispatcher. + * + * @author Wouter de Jong + */ +class FirewallEventBubblingListener implements EventSubscriberInterface +{ + private $eventDispatcher; + + public function __construct(EventDispatcherInterface $eventDispatcher) + { + $this->eventDispatcher = $eventDispatcher; + } + + public static function getSubscribedEvents(): array + { + return [ + LogoutEvent::class => 'bubbleEvent', + ]; + } + + public function bubbleEvent($event): void + { + $this->eventDispatcher->dispatch($event); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 1f0e64b803484..28dceee7de11c 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -90,6 +90,10 @@ + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index 6b4b441c98359..8b14cfd9e0c52 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml @@ -48,17 +48,17 @@ - + - + - + - + - / + / diff --git a/src/Symfony/Bundle/SecurityBundle/Security/LegacyLogoutHandlerListener.php b/src/Symfony/Bundle/SecurityBundle/Security/LegacyLogoutHandlerListener.php new file mode 100644 index 0000000000000..653abc2345a08 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Security/LegacyLogoutHandlerListener.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Security; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Http\Event\LogoutEvent; +use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface; +use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface; + +/** + * @author Wouter de Jong + * + * @internal + * @deprecated to be removed in version 6.0 + */ +class LegacyLogoutHandlerListener implements EventSubscriberInterface +{ + private $logoutHandler; + + public function __construct(object $logoutHandler) + { + if (!$logoutHandler instanceof LogoutSuccessHandlerInterface && !$logoutHandler instanceof LogoutHandlerInterface) { + throw new \InvalidArgumentException(sprintf('An instance of "%s" or "%s" must be passed to "%s", "%s" given.', LogoutHandlerInterface::class, LogoutSuccessHandlerInterface::class, __METHOD__, get_debug_type($logoutHandler))); + } + + $this->logoutHandler = $logoutHandler; + } + + public static function getSubscribedEvents(): array + { + return [ + LogoutEvent::class => 'onLogout', + ]; + } + + public function onLogout(LogoutEvent $event): void + { + if ($this->logoutHandler instanceof LogoutSuccessHandlerInterface) { + $event->setResponse($this->logoutHandler->onLogoutSuccess($event->getRequest())); + } elseif ($this->logoutHandler instanceof LogoutHandlerInterface) { + $this->logoutHandler->logout($event->getRequest(), $event->getResponse(), $event->getToken()); + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 0843a4659ad31..b06d8b4c3a05f 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -20,6 +20,7 @@ "ext-xml": "*", "symfony/config": "^4.4|^5.0", "symfony/dependency-injection": "^4.4|^5.0", + "symfony/event-dispatcher": "^5.1", "symfony/http-kernel": "^5.0", "symfony/polyfill-php80": "^1.15", "symfony/security-core": "^4.4|^5.0", diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php index e1918b6ca2111..2e4a56e591d03 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php @@ -122,7 +122,7 @@ public function process(ContainerBuilder $container) $dispatcherDefinition = $globalDispatcherDefinition; foreach ($tags as $attributes) { if (isset($attributes['dispatcher'])) { - $dispatcherDefinition = $container->getDefinition($event['dispatcher']); + $dispatcherDefinition = $container->getDefinition($attributes['dispatcher']); break; } } diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md index 9f81f45191b7d..7001030e9d5c9 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -7,6 +7,8 @@ CHANGELOG * Added access decision strategy to override access decisions by voter service priority * Added `IS_ANONYMOUS`, `IS_REMEMBERED`, `IS_IMPERSONATOR` * Hash the persistent RememberMe token value in database. + * Added LogoutEvent to allow custom logout listeners. + * Deprecated `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface` in favor of listening on the `LogoutEvent`. 5.0.0 ----- diff --git a/src/Symfony/Component/Security/Http/Event/LogoutEvent.php b/src/Symfony/Component/Security/Http/Event/LogoutEvent.php new file mode 100644 index 0000000000000..3c521f1c3198e --- /dev/null +++ b/src/Symfony/Component/Security/Http/Event/LogoutEvent.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Event; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Contracts\EventDispatcher\Event; + +/** + * @author Wouter de Jong + */ +class LogoutEvent extends Event +{ + private $request; + private $response; + private $token; + + public function __construct(Request $request, ?TokenInterface $token) + { + $this->request = $request; + $this->token = $token; + } + + public function getRequest(): Request + { + return $this->request; + } + + public function getToken(): ?TokenInterface + { + return $this->token; + } + + public function setResponse(Response $response): void + { + $this->response = $response; + } + + public function getResponse(): ?Response + { + return $this->response; + } +} diff --git a/src/Symfony/Component/Security/Http/EventListener/CookieClearingLogoutListener.php b/src/Symfony/Component/Security/Http/EventListener/CookieClearingLogoutListener.php new file mode 100644 index 0000000000000..b492052120565 --- /dev/null +++ b/src/Symfony/Component/Security/Http/EventListener/CookieClearingLogoutListener.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Http\Event\LogoutEvent; + +/** + * This listener clears the passed cookies when a user logs out. + * + * @author Johannes M. Schmitt + * + * @internal + */ +class CookieClearingLogoutListener implements EventSubscriberInterface +{ + private $cookies; + + /** + * @param array $cookies An array of cookies (keys are names, values contain path and domain) to unset + */ + public function __construct(array $cookies) + { + $this->cookies = $cookies; + } + + public static function getSubscribedEvents(): array + { + return [ + LogoutEvent::class => ['onLogout', -255], + ]; + } + + public function onLogout(LogoutEvent $event): void + { + if (!$response = $event->getResponse()) { + return; + } + + foreach ($this->cookies as $cookieName => $cookieData) { + $response->headers->clearCookie($cookieName, $cookieData['path'], $cookieData['domain']); + } + } +} diff --git a/src/Symfony/Component/Security/Http/EventListener/CsrfTokenClearingLogoutListener.php b/src/Symfony/Component/Security/Http/EventListener/CsrfTokenClearingLogoutListener.php new file mode 100644 index 0000000000000..61d0c55a7c9d7 --- /dev/null +++ b/src/Symfony/Component/Security/Http/EventListener/CsrfTokenClearingLogoutListener.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface; +use Symfony\Component\Security\Http\Event\LogoutEvent; + +/** + * @author Christian Flothmann + * + * @internal + */ +class CsrfTokenClearingLogoutListener implements EventSubscriberInterface +{ + private $csrfTokenStorage; + + public function __construct(ClearableTokenStorageInterface $csrfTokenStorage) + { + $this->csrfTokenStorage = $csrfTokenStorage; + } + + public static function getSubscribedEvents(): array + { + return [ + LogoutEvent::class => 'onLogout', + ]; + } + + public function onLogout(LogoutEvent $event): void + { + $this->csrfTokenStorage->clear(); + } +} diff --git a/src/Symfony/Component/Security/Http/EventListener/DefaultLogoutListener.php b/src/Symfony/Component/Security/Http/EventListener/DefaultLogoutListener.php new file mode 100644 index 0000000000000..b660f86c311e3 --- /dev/null +++ b/src/Symfony/Component/Security/Http/EventListener/DefaultLogoutListener.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Http\Event\LogoutEvent; +use Symfony\Component\Security\Http\HttpUtils; + +/** + * Default logout listener will redirect users to a configured path. + * + * @author Fabien Potencier + * @author Alexander + * + * @internal + */ +class DefaultLogoutListener implements EventSubscriberInterface +{ + private $httpUtils; + private $targetUrl; + + public function __construct(HttpUtils $httpUtils, string $targetUrl = '/') + { + $this->httpUtils = $httpUtils; + $this->targetUrl = $targetUrl; + } + + public static function getSubscribedEvents(): array + { + return [ + LogoutEvent::class => ['onLogout', -128], + ]; + } + + public function onLogout(LogoutEvent $event): void + { + if (null !== $event->getResponse()) { + return; + } + + $event->setResponse($this->httpUtils->createRedirectResponse($event->getRequest(), $this->targetUrl)); + } +} diff --git a/src/Symfony/Component/Security/Http/EventListener/RememberMeLogoutListener.php b/src/Symfony/Component/Security/Http/EventListener/RememberMeLogoutListener.php new file mode 100644 index 0000000000000..3d9bdfbb1b6f1 --- /dev/null +++ b/src/Symfony/Component/Security/Http/EventListener/RememberMeLogoutListener.php @@ -0,0 +1,34 @@ +rememberMeServices = $rememberMeServices; + } + + public static function getSubscribedEvents(): array + { + return [ + LogoutEvent::class => ['onLogout', -256], + ]; + } + + public function onLogout(LogoutEvent $event): void + { + if (null === $event->getResponse()) { + throw new LogicException(sprintf('No response was set for this logout action. Make sure the DefaultLogoutListener or another listener has set the response before "%s" is called.', __CLASS__)); + } + + $this->rememberMeServices->logout($event->getRequest(), $event->getResponse(), $event->getToken()); + } +} diff --git a/src/Symfony/Component/Security/Http/EventListener/SessionLogoutListener.php b/src/Symfony/Component/Security/Http/EventListener/SessionLogoutListener.php new file mode 100644 index 0000000000000..72eb99c269b53 --- /dev/null +++ b/src/Symfony/Component/Security/Http/EventListener/SessionLogoutListener.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\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Http\Event\LogoutEvent; + +/** + * Handler for clearing invalidating the current session. + * + * @author Johannes M. Schmitt + * + * @internal + */ +class SessionLogoutListener implements EventSubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + LogoutEvent::class => 'onLogout', + ]; + } + + public function onLogout(LogoutEvent $event): void + { + $event->getRequest()->getSession()->invalidate(); + } +} diff --git a/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php b/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php index 1194cea95f1e2..a8ba30322fe4f 100644 --- a/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Security\Http\Firewall; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\RequestEvent; @@ -18,6 +20,7 @@ use Symfony\Component\Security\Core\Exception\LogoutException; use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Security\Http\Event\LogoutEvent; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface; use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface; @@ -34,16 +37,29 @@ class LogoutListener extends AbstractListener { private $tokenStorage; private $options; - private $handlers; - private $successHandler; private $httpUtils; private $csrfTokenManager; + private $eventDispatcher; /** * @param array $options An array of options to process a logout attempt */ - public function __construct(TokenStorageInterface $tokenStorage, HttpUtils $httpUtils, LogoutSuccessHandlerInterface $successHandler, array $options = [], CsrfTokenManagerInterface $csrfTokenManager = null) + public function __construct(TokenStorageInterface $tokenStorage, HttpUtils $httpUtils, /* EventDispatcherInterface */$eventDispatcher, array $options = [], CsrfTokenManagerInterface $csrfTokenManager = null) { + if (!$eventDispatcher instanceof EventDispatcherInterface) { + trigger_deprecation('symfony/security-http', '5.1', 'Passing a logout success handler to "%s" is deprecated, pass an instance of "%s" instead.', __METHOD__, EventDispatcherInterface::class); + + if (!$eventDispatcher instanceof LogoutSuccessHandlerInterface) { + throw new \InvalidArgumentException(sprintf('Argument 3 of "%s" must be instance of "%s" or "%s", "%s" given.', __METHOD__, EventDispatcherInterface::class, LogoutSuccessHandlerInterface::class, get_debug_type($successHandler))); + } + + $successHandler = $eventDispatcher; + $eventDispatcher = new EventDispatcher(); + $eventDispatcher->addListener(LogoutEvent::class, function (LogoutEvent $event) use ($successHandler) { + $event->setResponse($r = $successHandler->onLogoutSuccess($event->getRequest())); + }); + } + $this->tokenStorage = $tokenStorage; $this->httpUtils = $httpUtils; $this->options = array_merge([ @@ -51,14 +67,17 @@ public function __construct(TokenStorageInterface $tokenStorage, HttpUtils $http 'csrf_token_id' => 'logout', 'logout_path' => '/logout', ], $options); - $this->successHandler = $successHandler; $this->csrfTokenManager = $csrfTokenManager; - $this->handlers = []; + $this->eventDispatcher = $eventDispatcher; } public function addHandler(LogoutHandlerInterface $handler) { - $this->handlers[] = $handler; + trigger_deprecation('symfony/security-http', '5.1', 'Calling "%s" is deprecated, register a listener on the "%s" event instead.', __METHOD__, LogoutEvent::class); + + $this->eventDispatcher->addListener(LogoutEvent::class, function (LogoutEvent $event) use ($handler) { + $handler->logout($event->getRequest(), $event->getResponse(), $event->getToken()); + }); } /** @@ -78,9 +97,9 @@ public function supports(Request $request): ?bool * @throws LogoutException if the CSRF token is invalid * @throws \RuntimeException if the LogoutSuccessHandlerInterface instance does not return a response */ - public function authenticate(RequestEvent $event) + public function authenticate(RequestEvent $requestEvent) { - $request = $event->getRequest(); + $request = $requestEvent->getRequest(); if (null !== $this->csrfTokenManager) { $csrfToken = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']); @@ -90,21 +109,17 @@ public function authenticate(RequestEvent $event) } } - $response = $this->successHandler->onLogoutSuccess($request); - if (!$response instanceof Response) { - throw new \RuntimeException('Logout Success Handler did not return a Response.'); - } + $event = new LogoutEvent($request, $this->tokenStorage->getToken()); + $this->eventDispatcher->dispatch($event); - // handle multiple logout attempts gracefully - if ($token = $this->tokenStorage->getToken()) { - foreach ($this->handlers as $handler) { - $handler->logout($request, $response, $token); - } + $response = $event->getResponse(); + if (!$response instanceof Response) { + throw new \RuntimeException('No Logout listener set the Response, make sure at least the DefaultLogoutListener is registered.'); } $this->tokenStorage->setToken(null); - $event->setResponse($response); + $requestEvent->setResponse($response); } /** diff --git a/src/Symfony/Component/Security/Http/Logout/LogoutHandlerInterface.php b/src/Symfony/Component/Security/Http/Logout/LogoutHandlerInterface.php index 92076a94ccc13..e7958f6651a62 100644 --- a/src/Symfony/Component/Security/Http/Logout/LogoutHandlerInterface.php +++ b/src/Symfony/Component/Security/Http/Logout/LogoutHandlerInterface.php @@ -14,11 +14,14 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Http\Event\LogoutEvent; /** * Interface that needs to be implemented by LogoutHandlers. * * @author Johannes M. Schmitt + * + * @deprecated since Symfony 5.1 */ interface LogoutHandlerInterface { diff --git a/src/Symfony/Component/Security/Http/Logout/LogoutSuccessHandlerInterface.php b/src/Symfony/Component/Security/Http/Logout/LogoutSuccessHandlerInterface.php index c320ad655f278..b5589b519dade 100644 --- a/src/Symfony/Component/Security/Http/Logout/LogoutSuccessHandlerInterface.php +++ b/src/Symfony/Component/Security/Http/Logout/LogoutSuccessHandlerInterface.php @@ -13,6 +13,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Http\Event\LogoutEvent; /** * LogoutSuccesshandlerInterface. @@ -24,6 +25,8 @@ * LogoutHandlerInterface instead. * * @author Johannes M. Schmitt + * + * @deprecated since Symfony 5.1. */ interface LogoutSuccessHandlerInterface { diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php index 3d51a26196a76..5b8a73e2795e9 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php @@ -12,21 +12,26 @@ namespace Symfony\Component\Security\Http\Tests\Firewall; use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\Security\Http\Event\LogoutEvent; use Symfony\Component\Security\Http\Firewall\LogoutListener; class LogoutListenerTest extends TestCase { public function testHandleUnmatchedPath() { - list($listener, , $httpUtils, $options) = $this->getListener(); + $dispatcher = $this->getEventDispatcher(); + list($listener, , $httpUtils, $options) = $this->getListener($dispatcher); list($event, $request) = $this->getGetResponseEvent(); - $event->expects($this->never()) - ->method('setResponse'); + $logoutEventDispatched = false; + $dispatcher->addListener(LogoutEvent::class, function (LogoutEvent $event) use (&$logoutEventDispatched) { + $logoutEventDispatched = true; + }); $httpUtils->expects($this->once()) ->method('checkRequestPath') @@ -34,14 +39,16 @@ public function testHandleUnmatchedPath() ->willReturn(false); $listener($event); + + $this->assertFalse($logoutEventDispatched, 'LogoutEvent was dispatched.'); } - public function testHandleMatchedPathWithSuccessHandlerAndCsrfValidation() + public function testHandleMatchedPathWithCsrfValidation() { - $successHandler = $this->getSuccessHandler(); $tokenManager = $this->getTokenManager(); + $dispatcher = $this->getEventDispatcher(); - list($listener, $tokenStorage, $httpUtils, $options) = $this->getListener($successHandler, $tokenManager); + list($listener, $tokenStorage, $httpUtils, $options) = $this->getListener($dispatcher, $tokenManager); list($event, $request) = $this->getGetResponseEvent(); @@ -56,20 +63,15 @@ public function testHandleMatchedPathWithSuccessHandlerAndCsrfValidation() ->method('isTokenValid') ->willReturn(true); - $successHandler->expects($this->once()) - ->method('onLogoutSuccess') - ->with($request) - ->willReturn($response = new Response()); + $response = new Response(); + $dispatcher->addListener(LogoutEvent::class, function (LogoutEvent $event) use ($response) { + $event->setResponse($response); + }); $tokenStorage->expects($this->once()) ->method('getToken') ->willReturn($token = $this->getToken()); - $handler = $this->getHandler(); - $handler->expects($this->once()) - ->method('logout') - ->with($request, $response, $token); - $tokenStorage->expects($this->once()) ->method('setToken') ->with(null); @@ -78,16 +80,13 @@ public function testHandleMatchedPathWithSuccessHandlerAndCsrfValidation() ->method('setResponse') ->with($response); - $listener->addHandler($handler); - $listener($event); } - public function testHandleMatchedPathWithoutSuccessHandlerAndCsrfValidation() + public function testHandleMatchedPathWithoutCsrfValidation() { - $successHandler = $this->getSuccessHandler(); - - list($listener, $tokenStorage, $httpUtils, $options) = $this->getListener($successHandler); + $dispatcher = $this->getEventDispatcher(); + list($listener, $tokenStorage, $httpUtils, $options) = $this->getListener($dispatcher); list($event, $request) = $this->getGetResponseEvent(); @@ -96,20 +95,15 @@ public function testHandleMatchedPathWithoutSuccessHandlerAndCsrfValidation() ->with($request, $options['logout_path']) ->willReturn(true); - $successHandler->expects($this->once()) - ->method('onLogoutSuccess') - ->with($request) - ->willReturn($response = new Response()); + $response = new Response(); + $dispatcher->addListener(LogoutEvent::class, function (LogoutEvent $event) use ($response) { + $event->setResponse($response); + }); $tokenStorage->expects($this->once()) ->method('getToken') ->willReturn($token = $this->getToken()); - $handler = $this->getHandler(); - $handler->expects($this->once()) - ->method('logout') - ->with($request, $response, $token); - $tokenStorage->expects($this->once()) ->method('setToken') ->with(null); @@ -118,17 +112,14 @@ public function testHandleMatchedPathWithoutSuccessHandlerAndCsrfValidation() ->method('setResponse') ->with($response); - $listener->addHandler($handler); - $listener($event); } - public function testSuccessHandlerReturnsNonResponse() + public function testNoResponseSet() { $this->expectException('RuntimeException'); - $successHandler = $this->getSuccessHandler(); - list($listener, , $httpUtils, $options) = $this->getListener($successHandler); + list($listener, , $httpUtils, $options) = $this->getListener(); list($event, $request) = $this->getGetResponseEvent(); @@ -137,11 +128,6 @@ public function testSuccessHandlerReturnsNonResponse() ->with($request, $options['logout_path']) ->willReturn(true); - $successHandler->expects($this->once()) - ->method('onLogoutSuccess') - ->with($request) - ->willReturn(null); - $listener($event); } @@ -203,12 +189,12 @@ private function getHttpUtils() ->getMock(); } - private function getListener($successHandler = null, $tokenManager = null) + private function getListener($eventDispatcher = null, $tokenManager = null) { $listener = new LogoutListener( $tokenStorage = $this->getTokenStorage(), $httpUtils = $this->getHttpUtils(), - $successHandler ?: $this->getSuccessHandler(), + $eventDispatcher ?? $this->getEventDispatcher(), $options = [ 'csrf_parameter' => '_csrf_token', 'csrf_token_id' => 'logout', @@ -221,9 +207,9 @@ private function getListener($successHandler = null, $tokenManager = null) return [$listener, $tokenStorage, $httpUtils, $options]; } - private function getSuccessHandler() + private function getEventDispatcher() { - return $this->getMockBuilder('Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface')->getMock(); + return new EventDispatcher(); } private function getToken()