Skip to content

Commit

Permalink
[Security] Allow LogoutListener to validate CSRF tokens
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jmikola committed Feb 15, 2012
1 parent b1f545b commit aaaa040
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 19 deletions.
Expand Up @@ -200,6 +200,9 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto
->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()
Expand Down
Expand Up @@ -277,8 +277,10 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a
$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);

Expand All @@ -287,6 +289,11 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a
$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')));
Expand Down
39 changes: 29 additions & 10 deletions src/Symfony/Component/Security/Http/Firewall/LogoutListener.php
Expand Up @@ -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.
Expand All @@ -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();
}

Expand All @@ -66,7 +72,12 @@ public function addHandler(LogoutHandlerInterface $handler)
/**
* 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)
{
Expand All @@ -76,6 +87,14 @@ public function handle(GetResponseEvent $event)
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);

Expand Down
Expand Up @@ -34,19 +34,27 @@ public function testHandleUnmatchedPath()
$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)
Expand Down Expand Up @@ -74,7 +82,7 @@ public function testHandleMatchedPathWithSuccessHandler()
$listener->handle($event);
}

public function testHandleMatchedPathWithoutSuccessHandler()
public function testHandleMatchedPathWithoutSuccessHandlerAndCsrfValidation()
{
list($listener, $context, $httpUtils, $options) = $this->getListener();

Expand Down Expand Up @@ -136,6 +144,37 @@ public function testSuccessHandlerReturnsNonResponse()
$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')
Expand Down Expand Up @@ -168,16 +207,19 @@ private function getHttpUtils()
->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);
Expand Down

0 comments on commit aaaa040

Please sign in to comment.