Skip to content

Commit 1fc7b86

Browse files
committed
feature #36243 [Security] Refactor logout listener to dispatch an event instead (wouterj)
This PR was squashed before being merged into the 5.1-dev branch. Discussion ---------- [Security] Refactor logout listener to dispatch an event instead | Q | A | ------------- | --- | Branch? | master | Bug fix? | yes (sort of...) | New feature? | yes | Deprecations? | yes | Tickets | Fix #25212, Fix #22473 | License | MIT | Doc PR | tbd The current `LogoutListener` has some extension points, but they are not really DX-friendly (ref #25212). It requires hacking a `addMethodCall('addHandler')` in the container builder to register a custom logout handler. Also, it is impossible to overwrite the default logout functionality from a bundle (ref #22473). This PR introduces a `LogoutEvent` that replaces both the `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface`. This provides a DX-friendly extension point and also cleans up the authentication factories (no more `addMethodCall()`'s). In order to allow different logout handlers for different firewalls, I created a specific event dispatcher for each firewall (as also shortly discussed in #33558). The `dispatcher` tag attribute allows you to specify which dispatcher it should be registered to (defaulting to the global dispatcher). The `EventBubblingLogoutListener` also dispatches logout events on the global dispatcher, to be used for listeners that should run on all firewalls. _@weaverryan and I discussed this feature while working on #33558, but figured it was unrelated and could be done while preservering BC. So that's why a separate PR is created._ Commits ------- a9f096e [Security] Refactor logout listener to dispatch an event instead
2 parents fdd8ac5 + a9f096e commit 1fc7b86

26 files changed

+590
-102
lines changed

UPGRADE-5.1.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ Security
115115
{% endif %}
116116
```
117117

118+
* Deprecated `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface`, register a listener on the `LogoutEvent` event instead.
119+
* Deprecated `DefaultLogoutSuccessHandler` in favor of `DefaultLogoutListener`.
120+
118121
Yaml
119122
----
120123

UPGRADE-6.0.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,5 @@ Security
8484
--------
8585

8686
* Removed `ROLE_PREVIOUS_ADMIN` role in favor of `IS_IMPERSONATOR` attribute
87+
* Removed `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface`, register a listener on the `LogoutEvent` event instead.
88+
* Removed `DefaultLogoutSuccessHandler` in favor of `DefaultLogoutListener`.

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfTokenClearingLogoutHandlerPass.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1515
use Symfony\Component\DependencyInjection\ContainerBuilder;
1616
use Symfony\Component\DependencyInjection\Reference;
17+
use Symfony\Component\Security\Http\EventListener\CsrfTokenClearingLogoutListener;
1718

1819
/**
1920
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
@@ -33,10 +34,9 @@ public function process(ContainerBuilder $container)
3334
return;
3435
}
3536

36-
$container->register('security.logout.handler.csrf_token_clearing', 'Symfony\Component\Security\Http\Logout\CsrfTokenClearingLogoutHandler')
37+
$container->register('security.logout.listener.csrf_token_clearing', CsrfTokenClearingLogoutListener::class)
3738
->addArgument(new Reference('security.csrf.token_storage'))
39+
->addTag('kernel.event_subscriber')
3840
->setPublic(false);
39-
40-
$container->findDefinition('security.logout_listener')->addMethodCall('addHandler', [new Reference('security.logout.handler.csrf_token_clearing')]);
4141
}
4242
}

src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
1717
use Symfony\Component\Config\Definition\ConfigurationInterface;
1818
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
19+
use Symfony\Component\Security\Http\Event\LogoutEvent;
1920
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy;
2021

2122
/**
@@ -205,7 +206,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto
205206
->scalarNode('csrf_token_id')->defaultValue('logout')->end()
206207
->scalarNode('path')->defaultValue('/logout')->end()
207208
->scalarNode('target')->defaultValue('/')->end()
208-
->scalarNode('success_handler')->end()
209+
->scalarNode('success_handler')->setDeprecated(sprintf('The "%%node%%" at path "%%path%%" is deprecated, register a listener on the "%s" event instead.', LogoutEvent::class))->end()
209210
->booleanNode('invalidate_session')->defaultTrue()->end()
210211
->end()
211212
->fixXmlConfig('delete_cookie')
@@ -230,7 +231,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto
230231
->fixXmlConfig('handler')
231232
->children()
232233
->arrayNode('handlers')
233-
->prototype('scalar')->end()
234+
->prototype('scalar')->setDeprecated(sprintf('The "%%node%%" at path "%%path%%" is deprecated, register a listener on the "%s" event instead.', LogoutEvent::class))->end()
234235
->end()
235236
->end()
236237
->end()

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
1616
use Symfony\Component\DependencyInjection\ChildDefinition;
1717
use Symfony\Component\DependencyInjection\ContainerBuilder;
18+
use Symfony\Component\DependencyInjection\Definition;
1819
use Symfony\Component\DependencyInjection\Reference;
1920
use Symfony\Component\HttpFoundation\Cookie;
21+
use Symfony\Component\Security\Http\EventListener\RememberMeLogoutListener;
2022

2123
class RememberMeFactory implements SecurityFactoryInterface
2224
{
@@ -55,13 +57,6 @@ public function create(ContainerBuilder $container, string $id, array $config, ?
5557
$rememberMeServicesId = $templateId.'.'.$id;
5658
}
5759

58-
if ($container->hasDefinition('security.logout_listener.'.$id)) {
59-
$container
60-
->getDefinition('security.logout_listener.'.$id)
61-
->addMethodCall('addHandler', [new Reference($rememberMeServicesId)])
62-
;
63-
}
64-
6560
$rememberMeServices = $container->setDefinition($rememberMeServicesId, new ChildDefinition($templateId));
6661
$rememberMeServices->replaceArgument(1, $config['secret']);
6762
$rememberMeServices->replaceArgument(2, $id);
@@ -116,6 +111,11 @@ public function create(ContainerBuilder $container, string $id, array $config, ?
116111
$listener->replaceArgument(1, new Reference($rememberMeServicesId));
117112
$listener->replaceArgument(5, $config['catch_exceptions']);
118113

114+
// remember-me logout listener
115+
$container->setDefinition('security.logout.listener.remember_me.'.$id, new Definition(RememberMeLogoutListener::class))
116+
->addArgument(new Reference($rememberMeServicesId))
117+
->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$id]);
118+
119119
return [$authProviderId, $listenerId, $defaultEntryPoint];
120120
}
121121

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory;
1515
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
1616
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
17+
use Symfony\Bundle\SecurityBundle\Security\LegacyLogoutHandlerListener;
1718
use Symfony\Bundle\SecurityBundle\SecurityUserValueResolver;
1819
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
1920
use Symfony\Component\Config\FileLocator;
@@ -26,6 +27,7 @@
2627
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
2728
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
2829
use Symfony\Component\DependencyInjection\Reference;
30+
use Symfony\Component\EventDispatcher\EventDispatcher;
2931
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
3032
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
3133
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
@@ -307,6 +309,12 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
307309

308310
$config->replaceArgument(5, $defaultProvider);
309311

312+
// Register Firewall-specific event dispatcher
313+
$firewallEventDispatcherId = 'security.event_dispatcher.'.$id;
314+
$container->register($firewallEventDispatcherId, EventDispatcher::class);
315+
$container->setDefinition($firewallEventDispatcherId.'.event_bubbling_listener', new ChildDefinition('security.event_dispatcher.event_bubbling_listener'))
316+
->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]);
317+
310318
// Register listeners
311319
$listeners = [];
312320
$listenerKeys = [];
@@ -334,44 +342,50 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
334342
if (isset($firewall['logout'])) {
335343
$logoutListenerId = 'security.logout_listener.'.$id;
336344
$logoutListener = $container->setDefinition($logoutListenerId, new ChildDefinition('security.logout_listener'));
345+
$logoutListener->replaceArgument(2, new Reference($firewallEventDispatcherId));
337346
$logoutListener->replaceArgument(3, [
338347
'csrf_parameter' => $firewall['logout']['csrf_parameter'],
339348
'csrf_token_id' => $firewall['logout']['csrf_token_id'],
340349
'logout_path' => $firewall['logout']['path'],
341350
]);
342351

343-
// add logout success handler
352+
// add default logout listener
344353
if (isset($firewall['logout']['success_handler'])) {
354+
// deprecated, to be removed in Symfony 6.0
345355
$logoutSuccessHandlerId = $firewall['logout']['success_handler'];
356+
$container->register('security.logout.listener.legacy_success_listener.'.$id, LegacyLogoutHandlerListener::class)
357+
->setArguments([new Reference($logoutSuccessHandlerId)])
358+
->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]);
346359
} else {
347-
$logoutSuccessHandlerId = 'security.logout.success_handler.'.$id;
348-
$logoutSuccessHandler = $container->setDefinition($logoutSuccessHandlerId, new ChildDefinition('security.logout.success_handler'));
349-
$logoutSuccessHandler->replaceArgument(1, $firewall['logout']['target']);
360+
$logoutSuccessListenerId = 'security.logout.listener.default.'.$id;
361+
$container->setDefinition($logoutSuccessListenerId, new ChildDefinition('security.logout.listener.default'))
362+
->replaceArgument(1, $firewall['logout']['target'])
363+
->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]);
350364
}
351-
$logoutListener->replaceArgument(2, new Reference($logoutSuccessHandlerId));
352365

353366
// add CSRF provider
354367
if (isset($firewall['logout']['csrf_token_generator'])) {
355368
$logoutListener->addArgument(new Reference($firewall['logout']['csrf_token_generator']));
356369
}
357370

358-
// add session logout handler
371+
// add session logout listener
359372
if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) {
360-
$logoutListener->addMethodCall('addHandler', [new Reference('security.logout.handler.session')]);
373+
$container->setDefinition('security.logout.listener.session.'.$id, new ChildDefinition('security.logout.listener.session'))
374+
->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]);
361375
}
362376

363-
// add cookie logout handler
377+
// add cookie logout listener
364378
if (\count($firewall['logout']['delete_cookies']) > 0) {
365-
$cookieHandlerId = 'security.logout.handler.cookie_clearing.'.$id;
366-
$cookieHandler = $container->setDefinition($cookieHandlerId, new ChildDefinition('security.logout.handler.cookie_clearing'));
367-
$cookieHandler->addArgument($firewall['logout']['delete_cookies']);
368-
369-
$logoutListener->addMethodCall('addHandler', [new Reference($cookieHandlerId)]);
379+
$container->setDefinition('security.logout.listener.cookie_clearing.'.$id, new ChildDefinition('security.logout.listener.cookie_clearing'))
380+
->addArgument($firewall['logout']['delete_cookies'])
381+
->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]);
370382
}
371383

372-
// add custom handlers
373-
foreach ($firewall['logout']['handlers'] as $handlerId) {
374-
$logoutListener->addMethodCall('addHandler', [new Reference($handlerId)]);
384+
// add custom listeners (deprecated)
385+
foreach ($firewall['logout']['handlers'] as $i => $handlerId) {
386+
$container->register('security.logout.listener.legacy_handler.'.$i, LegacyLogoutHandlerListener::class)
387+
->addArgument(new Reference($handlerId))
388+
->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]);
375389
}
376390

377391
// register with LogoutUrlGenerator
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\EventListener;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\Security\Http\Event\LogoutEvent;
16+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
17+
18+
/**
19+
* A listener that dispatches all security events from the firewall-specific
20+
* dispatcher on the global event dispatcher.
21+
*
22+
* @author Wouter de Jong <wouter@wouterj.nl>
23+
*/
24+
class FirewallEventBubblingListener implements EventSubscriberInterface
25+
{
26+
private $eventDispatcher;
27+
28+
public function __construct(EventDispatcherInterface $eventDispatcher)
29+
{
30+
$this->eventDispatcher = $eventDispatcher;
31+
}
32+
33+
public static function getSubscribedEvents(): array
34+
{
35+
return [
36+
LogoutEvent::class => 'bubbleEvent',
37+
];
38+
}
39+
40+
public function bubbleEvent($event): void
41+
{
42+
$this->eventDispatcher->dispatch($event);
43+
}
44+
}

src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@
9090
</service>
9191
<service id="Symfony\Component\Security\Http\Authentication\AuthenticationUtils" alias="security.authentication_utils" />
9292

93+
<service id="security.event_dispatcher.event_bubbling_listener" class="Symfony\Bundle\SecurityBundle\EventListener\FirewallEventBubblingListener" abstract="true">
94+
<argument type="service" id="event_dispatcher" />
95+
</service>
96+
9397
<!-- Authorization related services -->
9498
<service id="security.access.decision_manager" class="Symfony\Component\Security\Core\Authorization\AccessDecisionManager">
9599
<argument type="collection" />

src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,17 @@
4848
<service id="security.logout_listener" class="Symfony\Component\Security\Http\Firewall\LogoutListener" abstract="true">
4949
<argument type="service" id="security.token_storage" />
5050
<argument type="service" id="security.http_utils" />
51-
<argument type="service" id="security.logout.success_handler" />
51+
<argument /> <!-- event dispatcher -->
5252
<argument /> <!-- Options -->
5353
</service>
5454

55-
<service id="security.logout.handler.session" class="Symfony\Component\Security\Http\Logout\SessionLogoutHandler" />
55+
<service id="security.logout.listener.session" class="Symfony\Component\Security\Http\EventListener\SessionLogoutListener" abstract="true" />
5656

57-
<service id="security.logout.handler.cookie_clearing" class="Symfony\Component\Security\Http\Logout\CookieClearingLogoutHandler" abstract="true" />
57+
<service id="security.logout.listener.cookie_clearing" class="Symfony\Component\Security\Http\Logout\CookieClearingLogoutHandler" abstract="true" />
5858

59-
<service id="security.logout.success_handler" class="Symfony\Component\Security\Http\Logout\DefaultLogoutSuccessHandler" abstract="true">
59+
<service id="security.logout.listener.default" class="Symfony\Component\Security\Http\EventListener\DefaultLogoutListener" abstract="true">
6060
<argument type="service" id="security.http_utils" />
61-
<argument>/</argument>
61+
<argument>/</argument> <!-- target url -->
6262
</service>
6363

6464
<service id="security.authentication.form_entry_point" class="Symfony\Component\Security\Http\EntryPoint\FormAuthenticationEntryPoint" abstract="true">
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Security;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\Security\Http\Event\LogoutEvent;
16+
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
17+
use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface;
18+
19+
/**
20+
* @author Wouter de Jong <wouter@wouterj.nl>
21+
*
22+
* @internal
23+
*/
24+
class LegacyLogoutHandlerListener implements EventSubscriberInterface
25+
{
26+
private $logoutHandler;
27+
28+
public function __construct(object $logoutHandler)
29+
{
30+
if (!$logoutHandler instanceof LogoutSuccessHandlerInterface && !$logoutHandler instanceof LogoutHandlerInterface) {
31+
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)));
32+
}
33+
34+
$this->logoutHandler = $logoutHandler;
35+
}
36+
37+
public function onLogout(LogoutEvent $event): void
38+
{
39+
if ($this->logoutHandler instanceof LogoutSuccessHandlerInterface) {
40+
$event->setResponse($this->logoutHandler->onLogoutSuccess($event->getRequest()));
41+
} elseif ($this->logoutHandler instanceof LogoutHandlerInterface) {
42+
$this->logoutHandler->logout($event->getRequest(), $event->getResponse(), $event->getToken());
43+
}
44+
}
45+
46+
public static function getSubscribedEvents(): array
47+
{
48+
return [
49+
LogoutEvent::class => 'onLogout',
50+
];
51+
}
52+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy