[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())
|
->treatTrueLike(array())
|
||||||
->canBeUnset()
|
->canBeUnset()
|
||||||
->children()
|
->children()
|
||||||
|
->scalarNode('csrf_parameter')->defaultValue('_csrf_token')->end()
|
||||||
|
->scalarNode('csrf_provider')->cannotBeEmpty()->end()
|
||||||
|
->scalarNode('intention')->defaultValue('logout')->end()
|
||||||
->scalarNode('path')->defaultValue('/logout')->end()
|
->scalarNode('path')->defaultValue('/logout')->end()
|
||||||
->scalarNode('target')->defaultValue('/')->end()
|
->scalarNode('target')->defaultValue('/')->end()
|
||||||
->scalarNode('success_handler')->end()
|
->scalarNode('success_handler')->end()
|
||||||
|
@ -277,8 +277,10 @@ class SecurityExtension extends Extension
|
|||||||
$listenerId = 'security.logout_listener.'.$id;
|
$listenerId = 'security.logout_listener.'.$id;
|
||||||
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.logout_listener'));
|
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.logout_listener'));
|
||||||
$listener->replaceArgument(2, array(
|
$listener->replaceArgument(2, array(
|
||||||
'logout_path' => $firewall['logout']['path'],
|
'csrf_parameter' => $firewall['logout']['csrf_parameter'],
|
||||||
'target_url' => $firewall['logout']['target'],
|
'intention' => $firewall['logout']['intention'],
|
||||||
|
'logout_path' => $firewall['logout']['path'],
|
||||||
|
'target_url' => $firewall['logout']['target'],
|
||||||
));
|
));
|
||||||
$listeners[] = new Reference($listenerId);
|
$listeners[] = new Reference($listenerId);
|
||||||
|
|
||||||
@ -287,6 +289,11 @@ class SecurityExtension extends Extension
|
|||||||
$listener->replaceArgument(4, new Reference($firewall['logout']['success_handler']));
|
$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
|
// add session logout handler
|
||||||
if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) {
|
if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) {
|
||||||
$listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session')));
|
$listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session')));
|
||||||
|
@ -11,14 +11,15 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\Security\Http\Firewall;
|
namespace Symfony\Component\Security\Http\Firewall;
|
||||||
|
|
||||||
use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface;
|
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
|
||||||
|
|
||||||
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
|
|
||||||
use Symfony\Component\Security\Core\SecurityContextInterface;
|
|
||||||
use Symfony\Component\Security\Http\HttpUtils;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
|
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.
|
* LogoutListener logout users.
|
||||||
@ -32,24 +33,29 @@ class LogoutListener implements ListenerInterface
|
|||||||
private $handlers;
|
private $handlers;
|
||||||
private $successHandler;
|
private $successHandler;
|
||||||
private $httpUtils;
|
private $httpUtils;
|
||||||
|
private $csrfProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
* @param SecurityContextInterface $securityContext
|
* @param SecurityContextInterface $securityContext
|
||||||
* @param HttpUtils $httpUtils An HttpUtilsInterface instance
|
* @param HttpUtils $httpUtils An HttpUtilsInterface instance
|
||||||
* @param array $options An array of options for the processing of a logout attempt
|
* @param array $options An array of options to process a logout attempt
|
||||||
* @param LogoutSuccessHandlerInterface $successHandler
|
* @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->securityContext = $securityContext;
|
||||||
$this->httpUtils = $httpUtils;
|
$this->httpUtils = $httpUtils;
|
||||||
$this->options = array_merge(array(
|
$this->options = array_merge(array(
|
||||||
'logout_path' => '/logout',
|
'csrf_parameter' => '_csrf_token',
|
||||||
'target_url' => '/',
|
'intention' => 'logout',
|
||||||
|
'logout_path' => '/logout',
|
||||||
|
'target_url' => '/',
|
||||||
), $options);
|
), $options);
|
||||||
$this->successHandler = $successHandler;
|
$this->successHandler = $successHandler;
|
||||||
|
$this->csrfProvider = $csrfProvider;
|
||||||
$this->handlers = array();
|
$this->handlers = array();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +72,12 @@ class LogoutListener implements ListenerInterface
|
|||||||
/**
|
/**
|
||||||
* Performs the logout if requested
|
* 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
|
* @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)
|
public function handle(GetResponseEvent $event)
|
||||||
{
|
{
|
||||||
@ -76,6 +87,14 @@ class LogoutListener implements ListenerInterface
|
|||||||
return;
|
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) {
|
if (null !== $this->successHandler) {
|
||||||
$response = $this->successHandler->onLogoutSuccess($request);
|
$response = $this->successHandler->onLogoutSuccess($request);
|
||||||
|
|
||||||
|
@ -34,19 +34,27 @@ class LogoutListenerTest extends \PHPUnit_Framework_TestCase
|
|||||||
$listener->handle($event);
|
$listener->handle($event);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testHandleMatchedPathWithSuccessHandler()
|
public function testHandleMatchedPathWithSuccessHandlerAndCsrfValidation()
|
||||||
{
|
{
|
||||||
$successHandler = $this->getSuccessHandler();
|
$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();
|
list($event, $request) = $this->getGetResponseEvent();
|
||||||
|
|
||||||
|
$request->query->set('_csrf_token', $csrfToken = 'token');
|
||||||
|
|
||||||
$httpUtils->expects($this->once())
|
$httpUtils->expects($this->once())
|
||||||
->method('checkRequestPath')
|
->method('checkRequestPath')
|
||||||
->with($request, $options['logout_path'])
|
->with($request, $options['logout_path'])
|
||||||
->will($this->returnValue(true));
|
->will($this->returnValue(true));
|
||||||
|
|
||||||
|
$csrfProvider->expects($this->once())
|
||||||
|
->method('isCsrfTokenValid')
|
||||||
|
->with('logout', $csrfToken)
|
||||||
|
->will($this->returnValue(true));
|
||||||
|
|
||||||
$successHandler->expects($this->once())
|
$successHandler->expects($this->once())
|
||||||
->method('onLogoutSuccess')
|
->method('onLogoutSuccess')
|
||||||
->with($request)
|
->with($request)
|
||||||
@ -74,7 +82,7 @@ class LogoutListenerTest extends \PHPUnit_Framework_TestCase
|
|||||||
$listener->handle($event);
|
$listener->handle($event);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testHandleMatchedPathWithoutSuccessHandler()
|
public function testHandleMatchedPathWithoutSuccessHandlerAndCsrfValidation()
|
||||||
{
|
{
|
||||||
list($listener, $context, $httpUtils, $options) = $this->getListener();
|
list($listener, $context, $httpUtils, $options) = $this->getListener();
|
||||||
|
|
||||||
@ -136,6 +144,37 @@ class LogoutListenerTest extends \PHPUnit_Framework_TestCase
|
|||||||
$listener->handle($event);
|
$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()
|
private function getContext()
|
||||||
{
|
{
|
||||||
return $this->getMockBuilder('Symfony\Component\Security\Core\SecurityContext')
|
return $this->getMockBuilder('Symfony\Component\Security\Core\SecurityContext')
|
||||||
@ -168,16 +207,19 @@ class LogoutListenerTest extends \PHPUnit_Framework_TestCase
|
|||||||
->getMock();
|
->getMock();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getListener($successHandler = null)
|
private function getListener($successHandler = null, $csrfProvider = null)
|
||||||
{
|
{
|
||||||
$listener = new LogoutListener(
|
$listener = new LogoutListener(
|
||||||
$context = $this->getContext(),
|
$context = $this->getContext(),
|
||||||
$httpUtils = $this->getHttpUtils(),
|
$httpUtils = $this->getHttpUtils(),
|
||||||
$options = array(
|
$options = array(
|
||||||
'logout_path' => '/logout',
|
'csrf_parameter' => '_csrf_token',
|
||||||
'target_url' => '/',
|
'intention' => 'logout',
|
||||||
|
'logout_path' => '/logout',
|
||||||
|
'target_url' => '/',
|
||||||
),
|
),
|
||||||
$successHandler
|
$successHandler,
|
||||||
|
$csrfProvider
|
||||||
);
|
);
|
||||||
|
|
||||||
return array($listener, $context, $httpUtils, $options);
|
return array($listener, $context, $httpUtils, $options);
|
||||||
|
Reference in New Issue
Block a user