From d94420f3a5e1d2699a7cccabca451b1cc02b4222 Mon Sep 17 00:00:00 2001 From: "Johannes M. Schmitt" Date: Fri, 3 Dec 2010 11:52:17 +0100 Subject: [PATCH] logout refactoring --- .../Factory/SecurityFactoryInterface.php | 6 +- .../DependencyInjection/SecurityExtension.php | 25 +++++++- .../Resources/config/security.xml | 10 ++- .../Resources/config/security_templates.xml | 10 +++ .../Security/Firewall/LogoutListener.php | 31 ++++++++-- .../Logout/CookieClearingLogoutHandler.php | 61 +++++++++++++++++++ .../Logout/LogoutHandlerInterface.php | 36 +++++++++++ .../Security/Logout/SessionLogoutHandler.php | 37 +++++++++++ .../CookieClearingLogoutHandlerTest.php | 42 +++++++++++++ .../Logout/SessionLogoutHandlerTest.php | 31 ++++++++++ 10 files changed, 271 insertions(+), 18 deletions(-) create mode 100644 src/Symfony/Component/HttpKernel/Security/Logout/CookieClearingLogoutHandler.php create mode 100644 src/Symfony/Component/HttpKernel/Security/Logout/LogoutHandlerInterface.php create mode 100644 src/Symfony/Component/HttpKernel/Security/Logout/SessionLogoutHandler.php create mode 100644 tests/Symfony/Tests/Component/HttpKernel/Security/Logout/CookieClearingLogoutHandlerTest.php create mode 100644 tests/Symfony/Tests/Component/HttpKernel/Security/Logout/SessionLogoutHandlerTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php index 0d539f146cf6..ab5fb7e77813 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php @@ -20,9 +20,9 @@ */ interface SecurityFactoryInterface { - public function create(ContainerBuilder $container, $id, $config, $userProvider, $providerIds, $defaultEntryPoint); + function create(ContainerBuilder $container, $id, $config, $userProvider, $providerIds, $defaultEntryPoint); - public function getPosition(); + function getPosition(); - public function getKey(); + function getKey(); } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/SecurityExtension.php index f13c2a817fe2..9cb97e867826 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/SecurityExtension.php @@ -203,14 +203,31 @@ protected function createFirewall(ContainerBuilder $container, $firewall, $provi // Logout listener if (array_key_exists('logout', $firewall)) { - $listeners[] = new Reference('security.logout_listener'); + $listenerId = 'security.logout_listener.'.$id; + $listener = $container->setDefinition($listenerId, clone $container->getDefinition('security.logout_listener')); + + $listeners[] = new Reference($listenerId); + $arguments = $listener->getArguments(); if (isset($firewall['logout']['path'])) { - $container->setParameter('security.logout.path', $firewall['logout']['path']); + $arguments[1] = $firewall['logout']['path']; } if (isset($firewall['logout']['target'])) { - $container->setParameter('security.logout.target_path', $firewall['logout']['target']); + $arguments[2] = $firewall['logout']['target']; + } + $listener->setArguments($arguments); + + if (!isset($firewall['stateless']) || !$firewall['stateless']) { + $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session'))); + } + + if (count($cookies = $this->fixConfig($firewall['logout'], 'cookie')) > 0) { + $cookieHandlerId = 'security.logout.handler.cookie_clearing.'.$id; + $cookieHandler = $container->setDefinition($cookieHandlerId, clone $container->getDefinition('security.logout.handler.cookie_clearing')); + $cookieHandler->setArguments(array($cookies)); + + $listener->addMethodCall('addHandler', array(new Reference($cookieHandlerId))); } } @@ -426,6 +443,8 @@ protected function createAccessListener($container, $id, $providers) return $listenerId; } + + protected function createExceptionListener($container, $id, $defaultEntryPoint) { $exceptionListenerId = 'security.exception_listener.'.$id; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security.xml index 0efcc6396c03..18d63a375080 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security.xml @@ -49,6 +49,8 @@ Symfony\Component\HttpKernel\Security\Firewall\LogoutListener /logout / + Symfony\Component\HttpKernel\Security\Logout\SessionLogoutHandler + Symfony\Component\HttpKernel\Security\Logout\CookieClearingLogoutHandler Symfony\Component\HttpKernel\Security\Firewall\SwitchUserListener ROLE_ALLOWED_TO_SWITCH @@ -91,6 +93,8 @@ + + %security.anonymous.key% @@ -113,12 +117,6 @@ %security.authentication.digest_entry_point.key% - - - %security.logout.path% - %security.logout.target_path% - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_templates.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_templates.xml index af6084e382ba..602dc0523f86 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_templates.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_templates.xml @@ -20,6 +20,16 @@ + + + + %security.logout.path% + %security.logout.target_path% + + + + + diff --git a/src/Symfony/Component/HttpKernel/Security/Firewall/LogoutListener.php b/src/Symfony/Component/HttpKernel/Security/Firewall/LogoutListener.php index 5b5794ca8d49..5b6a2402fb20 100644 --- a/src/Symfony/Component/HttpKernel/Security/Firewall/LogoutListener.php +++ b/src/Symfony/Component/HttpKernel/Security/Firewall/LogoutListener.php @@ -2,6 +2,7 @@ namespace Symfony\Component\HttpKernel\Security\Firewall; +use Symfony\Component\HttpKernel\Security\Logout\LogoutHandlerInterface; use Symfony\Component\Security\SecurityContext; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\Event; @@ -26,10 +27,12 @@ class LogoutListener implements ListenerInterface protected $securityContext; protected $logoutPath; protected $targetUrl; + protected $handlers; /** * Constructor * + * @param SecurityContext $securityContext * @param string $logoutPath The path that starts the logout process * @param string $targetUrl The URL to redirect to after logout */ @@ -38,7 +41,19 @@ public function __construct(SecurityContext $securityContext, $logoutPath, $targ $this->securityContext = $securityContext; $this->logoutPath = $logoutPath; $this->targetUrl = $targetUrl; + $this->handlers = array(); } + + /** + * Adds a logout handler + * + * @param LogoutHandlerInterface $handler + * @return void + */ + public function addHandler(LogoutHandlerInterface $handler) + { + $this->handlers[] = $handler; + } /** * Registers a core.security listener. @@ -59,7 +74,7 @@ public function unregister(EventDispatcher $dispatcher) } /** - * + * Performs the logout if requested * * @param Event $event An Event instance */ @@ -70,13 +85,17 @@ public function handle(Event $event) if ($this->logoutPath !== $request->getPathInfo()) { return; } - - $this->securityContext->setToken(null); - $request->getSession()->invalidate(); - + $response = new Response(); $response->setRedirect(0 !== strpos($this->targetUrl, 'http') ? $request->getUriForPath($this->targetUrl) : $this->targetUrl, 302); - + + $token = $this->securityContext->getToken(); + + foreach ($this->handlers as $handler) { + $handler->logout($request, $response, $token); + } + + $this->securityContext->setToken(null); $event->setReturnValue($response); return true; diff --git a/src/Symfony/Component/HttpKernel/Security/Logout/CookieClearingLogoutHandler.php b/src/Symfony/Component/HttpKernel/Security/Logout/CookieClearingLogoutHandler.php new file mode 100644 index 000000000000..f95045ad478f --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Security/Logout/CookieClearingLogoutHandler.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * This handler cleares the passed cookies when a user logs out. + * + * @author Johannes M. Schmitt + */ +class CookieClearingLogoutHandler implements LogoutHandlerInterface +{ + protected $cookieNames; + + /** + * Constructor + * @param array $cookieNames An array of cookie names to unset + */ + public function __construct(array $cookieNames) + { + $this->cookieNames = $cookieNames; + } + + /** + * Returns the names of the cookies to unset + * @return array + */ + public function getCookieNames() + { + return $this->cookieNames; + } + + /** + * Implementation for the LogoutHandlerInterface. Deletes all requested cookies. + * + * @param Request $request + * @param Response $response + * @param TokenInterface $token + * @return void + */ + public function logout(Request $request, Response $response, TokenInterface $token) + { + $expires = time() - 86400; + + foreach ($this->cookieNames as $cookieName) { + $response->headers->setCookie($cookieName, '', null, $expires); + } + } +} \ No newline at end of file diff --git a/src/Symfony/Component/HttpKernel/Security/Logout/LogoutHandlerInterface.php b/src/Symfony/Component/HttpKernel/Security/Logout/LogoutHandlerInterface.php new file mode 100644 index 000000000000..67f6e3494714 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Security/Logout/LogoutHandlerInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Interface that needs to be implemented by LogoutHandlers. + * + * @author Johannes M. Schmitt + */ +interface LogoutHandlerInterface +{ + /** + * This method is called by the LogoutListener when a user has requested + * to be logged out. Usually, you would unset session variables, or remove + * cookies, etc. + * + * @param Request $request + * @param Response $response + * @param TokenInterface $token + * @return void + */ + function logout(Request $request, Response $response, TokenInterface $token); +} \ No newline at end of file diff --git a/src/Symfony/Component/HttpKernel/Security/Logout/SessionLogoutHandler.php b/src/Symfony/Component/HttpKernel/Security/Logout/SessionLogoutHandler.php new file mode 100644 index 000000000000..c92e2d6ec1b2 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Security/Logout/SessionLogoutHandler.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Handler for clearing invalidating the current session. + * + * @author Johannes M. Schmitt + */ +class SessionLogoutHandler implements LogoutHandlerInterface +{ + /** + * Invalidate the current session + * + * @param Request $request + * @param Response $response + * @param TokenInterface $token + * @return void + */ + public function logout(Request $request, Response $response, TokenInterface $token) + { + $request->getSession()->invalidate(); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/HttpKernel/Security/Logout/CookieClearingLogoutHandlerTest.php b/tests/Symfony/Tests/Component/HttpKernel/Security/Logout/CookieClearingLogoutHandlerTest.php new file mode 100644 index 000000000000..3d16b4850601 --- /dev/null +++ b/tests/Symfony/Tests/Component/HttpKernel/Security/Logout/CookieClearingLogoutHandlerTest.php @@ -0,0 +1,42 @@ +assertEquals($cookieNames, $handler->getCookieNames()); + } + + public function testLogout() + { + $request = new Request(); + $response = new Response(); + $token = $this->getMock('Symfony\Component\Security\Authentication\Token\TokenInterface'); + + $handler = new CookieClearingLogoutHandler(array('foo', 'foo2')); + + $this->assertFalse($response->headers->has('Set-Cookie')); + + $handler->logout($request, $response, $token); + + $headers = $response->headers->all(); + $cookies = $headers['set-cookie']; + $this->assertEquals(2, count($cookies)); + + $cookie = $cookies[0]; + $this->assertStringStartsWith('foo=;', $cookie); + + $cookie = $cookies[1]; + $this->assertStringStartsWith('foo2=;', $cookie); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/HttpKernel/Security/Logout/SessionLogoutHandlerTest.php b/tests/Symfony/Tests/Component/HttpKernel/Security/Logout/SessionLogoutHandlerTest.php new file mode 100644 index 000000000000..57b7d3aa7d9a --- /dev/null +++ b/tests/Symfony/Tests/Component/HttpKernel/Security/Logout/SessionLogoutHandlerTest.php @@ -0,0 +1,31 @@ +getMock('Symfony\Component\HttpFoundation\Request'); + $response = new Response(); + $session = $this->getMock('Symfony\Component\HttpFoundation\Session', array(), array(), '', false); + + $request + ->expects($this->once()) + ->method('getSession') + ->will($this->returnValue($session)) + ; + + $session + ->expects($this->once()) + ->method('invalidate') + ; + + $handler->logout($request, $response, $this->getMock('Symfony\Component\Security\Authentication\Token\TokenInterface')); + } +} \ No newline at end of file