[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:
parent
b1f545b677
commit
aaaa04003d
@ -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()
|
||||
|
@ -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')));
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user