[Security] Allow LogoutListener to validate CSRF tokens

This adds several new options to the logout listener, modeled after the form_login listener:

 * csrf_parameter
 * intention
 * csrf_provider

The "csrf_parameter" and "intention" have default values if omitted. By default, "csrf_provider" is empty and CSRF validation is disabled in LogoutListener (preserving BC). If a service ID is given for "csrf_provider", CSRF validation will be enabled. Invalid tokens will result in an InvalidCsrfTokenException being thrown before any logout handlers are invoked.
This commit is contained in:
Jeremy Mikola 2011-12-30 00:08:04 -05:00
parent b1f545b677
commit aaaa04003d
4 changed files with 90 additions and 19 deletions

View File

@ -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()

View File

@ -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')));

View File

@ -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);

View File

@ -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);