From: MatTheCat <math.lechat@gmail.com>
Date: Fri, 11 May 2018 20:46:08 +0200
Subject: [Security] Fix logout

Origin: backport, https://github.com/symfony/symfony/commit/9e88eb5aa980e09f0f9ac691ebbe08d1baac0e0a
---
 .../DependencyInjection/SecurityExtension.php      | 25 ++++++++--------
 .../SecurityBundle/Resources/config/security.xml   |  1 +
 .../SecurityBundle/Security/FirewallContext.php    |  7 +++--
 .../Bundle/SecurityBundle/Security/FirewallMap.php |  2 +-
 .../CompleteConfigurationTest.php                  |  1 -
 .../SecurityBundle/Tests/Functional/LogoutTest.php | 34 ++++++++++++++++++++++
 .../Functional/app/RememberMeLogout/bundles.php    | 18 ++++++++++++
 .../Functional/app/RememberMeLogout/config.yml     | 25 ++++++++++++++++
 .../Functional/app/RememberMeLogout/routing.yml    |  5 ++++
 src/Symfony/Component/Security/Http/Firewall.php   | 13 +++++++--
 .../Component/Security/Http/FirewallMap.php        | 14 ++++-----
 11 files changed, 118 insertions(+), 27 deletions(-)
 create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php
 create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php
 create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/config.yml
 create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/routing.yml

diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
index 5f45b1f..5910010 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
@@ -230,13 +230,14 @@ class SecurityExtension extends Extension
         $mapDef = $container->getDefinition('security.firewall.map');
         $map = $authenticationProviders = array();
         foreach ($firewalls as $name => $firewall) {
-            list($matcher, $listeners, $exceptionListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds);
+            list($matcher, $listeners, $exceptionListener, $logoutListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds);
 
             $contextId = 'security.firewall.map.context.'.$name;
             $context = $container->setDefinition($contextId, new DefinitionDecorator('security.firewall.context'));
             $context
                 ->replaceArgument(0, $listeners)
                 ->replaceArgument(1, $exceptionListener)
+                ->replaceArgument(2, $logoutListener)
             ;
             $map[$contextId] = $matcher;
         }
@@ -267,7 +268,7 @@ class SecurityExtension extends Extension
 
         // Security disabled?
         if (false === $firewall['security']) {
-            return array($matcher, array(), null);
+            return array($matcher, array(), null, null);
         }
 
         // Provider id (take the first registered provider if none defined)
@@ -294,15 +295,15 @@ class SecurityExtension extends Extension
         }
 
         // Logout listener
+        $logoutListenerId = null;
         if (isset($firewall['logout'])) {
-            $listenerId = 'security.logout_listener.'.$id;
-            $listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.logout_listener'));
-            $listener->replaceArgument(3, array(
+            $logoutListenerId = 'security.logout_listener.'.$id;
+            $logoutListener = $container->setDefinition($logoutListenerId, new DefinitionDecorator('security.logout_listener'));
+            $logoutListener->replaceArgument(3, array(
                 'csrf_parameter' => $firewall['logout']['csrf_parameter'],
                 'csrf_token_id' => $firewall['logout']['csrf_token_id'],
                 'logout_path' => $firewall['logout']['path'],
             ));
-            $listeners[] = new Reference($listenerId);
 
             // add logout success handler
             if (isset($firewall['logout']['success_handler'])) {
@@ -312,16 +313,16 @@ class SecurityExtension extends Extension
                 $logoutSuccessHandler = $container->setDefinition($logoutSuccessHandlerId, new DefinitionDecorator('security.logout.success_handler'));
                 $logoutSuccessHandler->replaceArgument(1, $firewall['logout']['target']);
             }
-            $listener->replaceArgument(2, new Reference($logoutSuccessHandlerId));
+            $logoutListener->replaceArgument(2, new Reference($logoutSuccessHandlerId));
 
             // add CSRF provider
             if (isset($firewall['logout']['csrf_token_generator'])) {
-                $listener->addArgument(new Reference($firewall['logout']['csrf_token_generator']));
+                $logoutListener->addArgument(new Reference($firewall['logout']['csrf_token_generator']));
             }
 
             // add session logout handler
             if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) {
-                $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session')));
+                $logoutListener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session')));
             }
 
             // add cookie logout handler
@@ -330,12 +331,12 @@ class SecurityExtension extends Extension
                 $cookieHandler = $container->setDefinition($cookieHandlerId, new DefinitionDecorator('security.logout.handler.cookie_clearing'));
                 $cookieHandler->addArgument($firewall['logout']['delete_cookies']);
 
-                $listener->addMethodCall('addHandler', array(new Reference($cookieHandlerId)));
+                $logoutListener->addMethodCall('addHandler', array(new Reference($cookieHandlerId)));
             }
 
             // add custom handlers
             foreach ($firewall['logout']['handlers'] as $handlerId) {
-                $listener->addMethodCall('addHandler', array(new Reference($handlerId)));
+                $logoutListener->addMethodCall('addHandler', array(new Reference($handlerId)));
             }
 
             // register with LogoutUrlGenerator
@@ -372,7 +373,7 @@ class SecurityExtension extends Extension
 
         $container->setAlias(new Alias('security.user_checker.'.$id, false), $firewall['user_checker']);
 
-        return array($matcher, $listeners, $exceptionListener);
+        return array($matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null);
     }
 
     private function createContextListener($container, $contextKey)
diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
index bbcae99..029395d 100644
--- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
+++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
@@ -152,6 +152,7 @@
         <service id="security.firewall.context" class="%security.firewall.context.class%" abstract="true">
             <argument type="collection" />
             <argument type="service" id="security.exception_listener" />
+            <argument />  <!-- LogoutListener -->
         </service>
 
         <service id="security.logout_url_generator" class="Symfony\Component\Security\Http\Logout\LogoutUrlGenerator" public="false">
diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php
index 13d096d..e9f8fe6 100644
--- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php
+++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php
@@ -12,6 +12,7 @@
 namespace Symfony\Bundle\SecurityBundle\Security;
 
 use Symfony\Component\Security\Http\Firewall\ExceptionListener;
+use Symfony\Component\Security\Http\Firewall\LogoutListener;
 
 /**
  * This is a wrapper around the actual firewall configuration which allows us
@@ -23,15 +24,17 @@ class FirewallContext
 {
     private $listeners;
     private $exceptionListener;
+    private $logoutListener;
 
-    public function __construct(array $listeners, ExceptionListener $exceptionListener = null)
+    public function __construct(array $listeners, ExceptionListener $exceptionListener = null, LogoutListener $logoutListener = null)
     {
         $this->listeners = $listeners;
         $this->exceptionListener = $exceptionListener;
+        $this->logoutListener = $logoutListener;
     }
 
     public function getContext()
     {
-        return array($this->listeners, $this->exceptionListener);
+        return array($this->listeners, $this->exceptionListener, $this->logoutListener);
     }
 }
diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php
index dc87681..d45d7b8 100644
--- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php
+++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php
@@ -41,6 +41,6 @@ class FirewallMap implements FirewallMapInterface
             }
         }
 
-        return array(array(), null);
+        return array(array(), null, null);
     }
 }
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php
index 5e081be..99a9810 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php
@@ -77,7 +77,6 @@ abstract class CompleteConfigurationTest extends \PHPUnit_Framework_TestCase
             array(),
             array(
                 'security.channel_listener',
-                'security.logout_listener.secure',
                 'security.authentication.listener.x509.secure',
                 'security.authentication.listener.remote_user.secure',
                 'security.authentication.listener.form.secure',
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php
new file mode 100644
index 0000000..7eeb7c2
--- /dev/null
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php
@@ -0,0 +1,34 @@
+<?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\Tests\Functional;
+
+class LogoutTest extends WebTestCase
+{
+    public function testSessionLessRememberMeLogout()
+    {
+        $client = $this->createClient(array('test_case' => 'RememberMeLogout', 'root_config' => 'config.yml'));
+
+        $client->request('POST', '/login', array(
+            '_username' => 'johannes',
+            '_password' => 'test',
+        ));
+
+        $cookieJar = $client->getCookieJar();
+        $cookieJar->expire(session_name());
+
+        $this->assertNotNull($cookieJar->get('REMEMBERME'));
+
+        $client->request('GET', '/logout');
+
+        $this->assertNull($cookieJar->get('REMEMBERME'));
+    }
+}
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php
new file mode 100644
index 0000000..d90f774
--- /dev/null
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php
@@ -0,0 +1,18 @@
+<?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.
+ */
+
+use Symfony\Bundle\SecurityBundle\SecurityBundle;
+use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
+
+return array(
+    new FrameworkBundle(),
+    new SecurityBundle(),
+);
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/config.yml
new file mode 100644
index 0000000..48fd4ed
--- /dev/null
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/config.yml
@@ -0,0 +1,25 @@
+imports:
+    - { resource: ./../config/framework.yml }
+
+security:
+    encoders:
+        Symfony\Component\Security\Core\User\User: plaintext
+
+    providers:
+        in_memory:
+            memory:
+                users:
+                    johannes: { password: test, roles: [ROLE_USER] }
+
+    firewalls:
+        default:
+            form_login:
+                check_path: login
+                remember_me: true
+                require_previous_session: false
+            remember_me:
+                always_remember_me: true
+                key: key
+            logout: ~
+            anonymous: ~
+            stateless: true
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/routing.yml
new file mode 100644
index 0000000..1dddfca
--- /dev/null
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/routing.yml
@@ -0,0 +1,5 @@
+login:
+    path: /login
+
+logout:
+    path: /logout
diff --git a/src/Symfony/Component/Security/Http/Firewall.php b/src/Symfony/Component/Security/Http/Firewall.php
index 7bad47a..0145efd 100644
--- a/src/Symfony/Component/Security/Http/Firewall.php
+++ b/src/Symfony/Component/Security/Http/Firewall.php
@@ -58,20 +58,29 @@ class Firewall implements EventSubscriberInterface
         }
 
         // register listeners for this firewall
-        list($listeners, $exceptionListener) = $this->map->getListeners($event->getRequest());
+        $listeners = $this->map->getListeners($event->getRequest());
+
+        $authenticationListeners = $listeners[0];
+        $exceptionListener = $listeners[1];
+        $logoutListener = isset($listeners[2]) ? $listeners[2] : null;
+
         if (null !== $exceptionListener) {
             $this->exceptionListeners[$event->getRequest()] = $exceptionListener;
             $exceptionListener->register($this->dispatcher);
         }
 
         // initiate the listener chain
-        foreach ($listeners as $listener) {
+        foreach ($authenticationListeners as $listener) {
             $listener->handle($event);
 
             if ($event->hasResponse()) {
                 break;
             }
         }
+
+        if (null !== $logoutListener) {
+            $logoutListener->handle($event);
+        }
     }
 
     public function onKernelFinishRequest(FinishRequestEvent $event)
diff --git a/src/Symfony/Component/Security/Http/FirewallMap.php b/src/Symfony/Component/Security/Http/FirewallMap.php
index 1bb73bd..fc97410 100644
--- a/src/Symfony/Component/Security/Http/FirewallMap.php
+++ b/src/Symfony/Component/Security/Http/FirewallMap.php
@@ -14,6 +14,7 @@ namespace Symfony\Component\Security\Http;
 use Symfony\Component\HttpFoundation\RequestMatcherInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\Security\Http\Firewall\ExceptionListener;
+use Symfony\Component\Security\Http\Firewall\LogoutListener;
 
 /**
  * FirewallMap allows configuration of different firewalls for specific parts
@@ -25,14 +26,9 @@ class FirewallMap implements FirewallMapInterface
 {
     private $map = array();
 
-    /**
-     * @param RequestMatcherInterface $requestMatcher
-     * @param array                   $listeners
-     * @param ExceptionListener       $exceptionListener
-     */
-    public function add(RequestMatcherInterface $requestMatcher = null, array $listeners = array(), ExceptionListener $exceptionListener = null)
+    public function add(RequestMatcherInterface $requestMatcher = null, array $listeners = array(), ExceptionListener $exceptionListener = null, LogoutListener $logoutListener = null)
     {
-        $this->map[] = array($requestMatcher, $listeners, $exceptionListener);
+        $this->map[] = array($requestMatcher, $listeners, $exceptionListener, $logoutListener);
     }
 
     /**
@@ -42,10 +38,10 @@ class FirewallMap implements FirewallMapInterface
     {
         foreach ($this->map as $elements) {
             if (null === $elements[0] || $elements[0]->matches($request)) {
-                return array($elements[1], $elements[2]);
+                return array($elements[1], $elements[2], $elements[3]);
             }
         }
 
-        return array(array(), null);
+        return array(array(), null, null);
     }
 }
