diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 70065d8e17..5f975628ef 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -200,6 +200,9 @@ class MainConfiguration implements ConfigurationInterface ->treatTrueLike(array()) ->canBeUnset() ->children() + ->scalarNode('csrf_parameter')->defaultValue('_csrf_token')->end() + ->scalarNode('csrf_provider')->cannotBeEmpty()->end() + ->scalarNode('intention')->defaultValue('logout')->end() ->scalarNode('path')->defaultValue('/logout')->end() ->scalarNode('target')->defaultValue('/')->end() ->scalarNode('success_handler')->end() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 3c3f30f106..abdca9a2e9 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -277,8 +277,10 @@ class SecurityExtension extends Extension $listenerId = 'security.logout_listener.'.$id; $listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.logout_listener')); $listener->replaceArgument(2, array( - 'logout_path' => $firewall['logout']['path'], - 'target_url' => $firewall['logout']['target'], + 'csrf_parameter' => $firewall['logout']['csrf_parameter'], + 'intention' => $firewall['logout']['intention'], + 'logout_path' => $firewall['logout']['path'], + 'target_url' => $firewall['logout']['target'], )); $listeners[] = new Reference($listenerId); @@ -287,6 +289,11 @@ class SecurityExtension extends Extension $listener->replaceArgument(4, new Reference($firewall['logout']['success_handler'])); } + // add CSRF provider + if (isset($firewall['logout']['csrf_provider'])) { + $listener->addArgument(new Reference($firewall['logout']['csrf_provider'])); + } + // add session logout handler if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) { $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session'))); diff --git a/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php b/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php index d79445724b..f4d0b2cced 100644 --- a/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php @@ -11,14 +11,15 @@ namespace Symfony\Component\Security\Http\Firewall; -use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface; - -use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface; -use Symfony\Component\Security\Core\SecurityContextInterface; -use Symfony\Component\Security\Http\HttpUtils; +use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\Security\Core\SecurityContextInterface; +use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; +use Symfony\Component\Security\Http\HttpUtils; +use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface; +use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface; /** * LogoutListener logout users. @@ -32,24 +33,29 @@ class LogoutListener implements ListenerInterface private $handlers; private $successHandler; private $httpUtils; + private $csrfProvider; /** * Constructor * * @param SecurityContextInterface $securityContext * @param HttpUtils $httpUtils An HttpUtilsInterface instance - * @param array $options An array of options for the processing of a logout attempt - * @param LogoutSuccessHandlerInterface $successHandler + * @param array $options An array of options to process a logout attempt + * @param LogoutSuccessHandlerInterface $successHandler A LogoutSuccessHandlerInterface instance + * @param CsrfProviderInterface $csrfProvider A CsrfProviderInterface instance */ - public function __construct(SecurityContextInterface $securityContext, HttpUtils $httpUtils, array $options = array(), LogoutSuccessHandlerInterface $successHandler = null) + public function __construct(SecurityContextInterface $securityContext, HttpUtils $httpUtils, array $options = array(), LogoutSuccessHandlerInterface $successHandler = null, CsrfProviderInterface $csrfProvider = null) { $this->securityContext = $securityContext; $this->httpUtils = $httpUtils; $this->options = array_merge(array( - 'logout_path' => '/logout', - 'target_url' => '/', + 'csrf_parameter' => '_csrf_token', + 'intention' => 'logout', + 'logout_path' => '/logout', + 'target_url' => '/', ), $options); $this->successHandler = $successHandler; + $this->csrfProvider = $csrfProvider; $this->handlers = array(); } @@ -66,7 +72,12 @@ class LogoutListener implements ListenerInterface /** * Performs the logout if requested * + * If a CsrfProviderInterface instance is available, it will be used to + * validate the request. + * * @param GetResponseEvent $event A GetResponseEvent instance + * @throws InvalidCsrfTokenException if the CSRF token is invalid + * @throws RuntimeException if the LogoutSuccessHandlerInterface instance does not return a response */ public function handle(GetResponseEvent $event) { @@ -76,6 +87,14 @@ class LogoutListener implements ListenerInterface return; } + if (null !== $this->csrfProvider) { + $csrfToken = $request->get($this->options['csrf_parameter'], null, true); + + if (false === $this->csrfProvider->isCsrfTokenValid($this->options['intention'], $csrfToken)) { + throw new InvalidCsrfTokenException('Invalid CSRF token.'); + } + } + if (null !== $this->successHandler) { $response = $this->successHandler->onLogoutSuccess($request); diff --git a/tests/Symfony/Tests/Component/Security/Http/Firewall/LogoutListenerTest.php b/tests/Symfony/Tests/Component/Security/Http/Firewall/LogoutListenerTest.php index 465ba9e6b8..2e275db93b 100644 --- a/tests/Symfony/Tests/Component/Security/Http/Firewall/LogoutListenerTest.php +++ b/tests/Symfony/Tests/Component/Security/Http/Firewall/LogoutListenerTest.php @@ -34,19 +34,27 @@ class LogoutListenerTest extends \PHPUnit_Framework_TestCase $listener->handle($event); } - public function testHandleMatchedPathWithSuccessHandler() + public function testHandleMatchedPathWithSuccessHandlerAndCsrfValidation() { $successHandler = $this->getSuccessHandler(); + $csrfProvider = $this->getCsrfProvider(); - list($listener, $context, $httpUtils, $options) = $this->getListener($successHandler); + list($listener, $context, $httpUtils, $options) = $this->getListener($successHandler, $csrfProvider); list($event, $request) = $this->getGetResponseEvent(); + $request->query->set('_csrf_token', $csrfToken = 'token'); + $httpUtils->expects($this->once()) ->method('checkRequestPath') ->with($request, $options['logout_path']) ->will($this->returnValue(true)); + $csrfProvider->expects($this->once()) + ->method('isCsrfTokenValid') + ->with('logout', $csrfToken) + ->will($this->returnValue(true)); + $successHandler->expects($this->once()) ->method('onLogoutSuccess') ->with($request) @@ -74,7 +82,7 @@ class LogoutListenerTest extends \PHPUnit_Framework_TestCase $listener->handle($event); } - public function testHandleMatchedPathWithoutSuccessHandler() + public function testHandleMatchedPathWithoutSuccessHandlerAndCsrfValidation() { list($listener, $context, $httpUtils, $options) = $this->getListener(); @@ -136,6 +144,37 @@ class LogoutListenerTest extends \PHPUnit_Framework_TestCase $listener->handle($event); } + /** + * @expectedException Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException + */ + public function testCsrfValidationFails() + { + $csrfProvider = $this->getCsrfProvider(); + + list($listener, $context, $httpUtils, $options) = $this->getListener(null, $csrfProvider); + + list($event, $request) = $this->getGetResponseEvent(); + + $request->query->set('_csrf_token', $csrfToken = 'token'); + + $httpUtils->expects($this->once()) + ->method('checkRequestPath') + ->with($request, $options['logout_path']) + ->will($this->returnValue(true)); + + $csrfProvider->expects($this->once()) + ->method('isCsrfTokenValid') + ->with('logout', $csrfToken) + ->will($this->returnValue(false)); + + $listener->handle($event); + } + + private function getCsrfProvider() + { + return $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'); + } + private function getContext() { return $this->getMockBuilder('Symfony\Component\Security\Core\SecurityContext') @@ -168,16 +207,19 @@ class LogoutListenerTest extends \PHPUnit_Framework_TestCase ->getMock(); } - private function getListener($successHandler = null) + private function getListener($successHandler = null, $csrfProvider = null) { $listener = new LogoutListener( $context = $this->getContext(), $httpUtils = $this->getHttpUtils(), $options = array( - 'logout_path' => '/logout', - 'target_url' => '/', + 'csrf_parameter' => '_csrf_token', + 'intention' => 'logout', + 'logout_path' => '/logout', + 'target_url' => '/', ), - $successHandler + $successHandler, + $csrfProvider ); return array($listener, $context, $httpUtils, $options);