Skip to content

Commit

Permalink
[Security] Refactor logout listener to dispatch an event instead
Browse files Browse the repository at this point in the history
  • Loading branch information
wouterj authored and fabpot committed Apr 4, 2020
1 parent eb0e47b commit a9f096e
Show file tree
Hide file tree
Showing 26 changed files with 590 additions and 102 deletions.
3 changes: 3 additions & 0 deletions UPGRADE-5.1.md
Expand Up @@ -91,6 +91,9 @@ Security
{% endif %}
```

* Deprecated `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface`, register a listener on the `LogoutEvent` event instead.
* Deprecated `DefaultLogoutSuccessHandler` in favor of `DefaultLogoutListener`.

Yaml
----

Expand Down
2 changes: 2 additions & 0 deletions UPGRADE-6.0.md
Expand Up @@ -60,3 +60,5 @@ Security
--------

* Removed `ROLE_PREVIOUS_ADMIN` role in favor of `IS_IMPERSONATOR` attribute
* Removed `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface`, register a listener on the `LogoutEvent` event instead.
* Removed `DefaultLogoutSuccessHandler` in favor of `DefaultLogoutListener`.
Expand Up @@ -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 <christian.flothmann@sensiolabs.de>
Expand All @@ -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')]);
}
}
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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')
Expand All @@ -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()
Expand Down
Expand Up @@ -15,8 +15,10 @@
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\Security\Http\EventListener\RememberMeLogoutListener;

class RememberMeFactory implements SecurityFactoryInterface
{
Expand Down Expand Up @@ -55,13 +57,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);
Expand Down Expand Up @@ -116,6 +111,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->setDefinition('security.logout.listener.remember_me.'.$id, new Definition(RememberMeLogoutListener::class))
->addArgument(new Reference($rememberMeServicesId))
->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$id]);

return [$authProviderId, $listenerId, $defaultEntryPoint];
}

Expand Down
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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
Expand Down
@@ -0,0 +1,44 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <wouter@wouterj.nl>
*/
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);
}
}
Expand Up @@ -90,6 +90,10 @@
</service>
<service id="Symfony\Component\Security\Http\Authentication\AuthenticationUtils" alias="security.authentication_utils" />

<service id="security.event_dispatcher.event_bubbling_listener" class="Symfony\Bundle\SecurityBundle\EventListener\FirewallEventBubblingListener" abstract="true">
<argument type="service" id="event_dispatcher" />
</service>

<!-- Authorization related services -->
<service id="security.access.decision_manager" class="Symfony\Component\Security\Core\Authorization\AccessDecisionManager">
<argument type="collection" />
Expand Down
Expand Up @@ -48,17 +48,17 @@
<service id="security.logout_listener" class="Symfony\Component\Security\Http\Firewall\LogoutListener" abstract="true">
<argument type="service" id="security.token_storage" />
<argument type="service" id="security.http_utils" />
<argument type="service" id="security.logout.success_handler" />
<argument /> <!-- event dispatcher -->
<argument /> <!-- Options -->
</service>

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

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

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

<service id="security.authentication.form_entry_point" class="Symfony\Component\Security\Http\EntryPoint\FormAuthenticationEntryPoint" abstract="true">
Expand Down
@@ -0,0 +1,52 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <wouter@wouterj.nl>
*
* @internal
*/
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 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());
}
}

public static function getSubscribedEvents(): array
{
return [
LogoutEvent::class => 'onLogout',
];
}
}
1 change: 1 addition & 0 deletions src/Symfony/Bundle/SecurityBundle/composer.json
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/EventDispatcher/CHANGELOG.md
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
-----

* The `LegacyEventDispatcherProxy` class has been deprecated.
* Added an optional `dispatcher` attribute to the listener and subscriber tags in `RegisterListenerPass`.

5.0.0
-----
Expand Down

0 comments on commit a9f096e

Please sign in to comment.