Merge branch '4.1'
* 4.1: clear CSRF tokens when the user is logged out
This commit is contained in:
commit
562b1f195f
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
|
||||
*/
|
||||
class RegisterCsrfTokenClearingLogoutHandlerPass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
if (!$container->has('security.logout_listener') || !$container->has('security.csrf.token_storage')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$csrfTokenStorage = $container->findDefinition('security.csrf.token_storage');
|
||||
$csrfTokenStorageClass = $container->getParameterBag()->resolveValue($csrfTokenStorage->getClass());
|
||||
|
||||
if (!is_subclass_of($csrfTokenStorageClass, 'Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$container->register('security.logout.handler.csrf_token_clearing', 'Symfony\Component\Security\Http\Logout\CsrfTokenClearingLogoutHandler')
|
||||
->addArgument(new Reference('security.csrf.token_storage'))
|
||||
->setPublic(false);
|
||||
|
||||
$container->findDefinition('security.logout_listener')->addMethodCall('addHandler', array(new Reference('security.logout.handler.csrf_token_clearing')));
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Bundle\SecurityBundle;
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfTokenClearingLogoutHandlerPass;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\JsonLoginFactory;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
@ -59,6 +60,7 @@ class SecurityBundle extends Bundle
|
||||
$extension->addUserProviderFactory(new LdapFactory());
|
||||
$container->addCompilerPass(new AddSecurityVotersPass());
|
||||
$container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_AFTER_REMOVING);
|
||||
$container->addCompilerPass(new RegisterCsrfTokenClearingLogoutHandlerPass());
|
||||
}
|
||||
|
||||
public function registerCommands(Application $application)
|
||||
|
@ -31,4 +31,22 @@ class LogoutTest extends WebTestCase
|
||||
|
||||
$this->assertNull($cookieJar->get('REMEMBERME'));
|
||||
}
|
||||
|
||||
public function testCsrfTokensAreClearedOnLogout()
|
||||
{
|
||||
$client = $this->createClient(array('test_case' => 'LogoutWithoutSessionInvalidation', 'root_config' => 'config.yml'));
|
||||
$client->getContainer()->get('security.csrf.token_storage')->setToken('foo', 'bar');
|
||||
|
||||
$client->request('POST', '/login', array(
|
||||
'_username' => 'johannes',
|
||||
'_password' => 'test',
|
||||
));
|
||||
|
||||
$this->assertTrue($client->getContainer()->get('security.csrf.token_storage')->hasToken('foo'));
|
||||
$this->assertSame('bar', $client->getContainer()->get('security.csrf.token_storage')->getToken('foo'));
|
||||
|
||||
$client->request('GET', '/logout');
|
||||
|
||||
$this->assertFalse($client->getContainer()->get('security.csrf.token_storage')->hasToken('foo'));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
||||
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||
|
||||
return array(
|
||||
new FrameworkBundle(),
|
||||
new SecurityBundle(),
|
||||
);
|
@ -0,0 +1,26 @@
|
||||
imports:
|
||||
- { resource: ./../config/framework.yml }
|
||||
|
||||
security:
|
||||
encoders:
|
||||
Symfony\Component\Security\Core\User\User: plaintext
|
||||
|
||||
providers:
|
||||
in_memory:
|
||||
memory:
|
||||
users:
|
||||
johannes: { password: test, roles: [ROLE_USER] }
|
||||
|
||||
firewalls:
|
||||
default:
|
||||
form_login:
|
||||
check_path: login
|
||||
remember_me: true
|
||||
require_previous_session: false
|
||||
remember_me:
|
||||
always_remember_me: true
|
||||
key: key
|
||||
logout:
|
||||
invalidate_session: false
|
||||
anonymous: ~
|
||||
stateless: true
|
@ -0,0 +1,5 @@
|
||||
login:
|
||||
path: /login
|
||||
|
||||
logout:
|
||||
path: /logout
|
@ -113,4 +113,32 @@ class NativeSessionTokenStorageTest extends TestCase
|
||||
$this->assertSame('TOKEN', $this->storage->removeToken('token_id'));
|
||||
$this->assertFalse($this->storage->hasToken('token_id'));
|
||||
}
|
||||
|
||||
public function testClearRemovesAllTokensFromTheConfiguredNamespace()
|
||||
{
|
||||
$this->storage->setToken('foo', 'bar');
|
||||
$this->storage->clear();
|
||||
|
||||
$this->assertFalse($this->storage->hasToken('foo'));
|
||||
$this->assertArrayNotHasKey(self::SESSION_NAMESPACE, $_SESSION);
|
||||
}
|
||||
|
||||
public function testClearDoesNotRemoveSessionValuesFromOtherNamespaces()
|
||||
{
|
||||
$_SESSION['foo']['bar'] = 'baz';
|
||||
$this->storage->clear();
|
||||
|
||||
$this->assertArrayHasKey('foo', $_SESSION);
|
||||
$this->assertArrayHasKey('bar', $_SESSION['foo']);
|
||||
$this->assertSame('baz', $_SESSION['foo']['bar']);
|
||||
}
|
||||
|
||||
public function testClearDoesNotRemoveNonNamespacedSessionValues()
|
||||
{
|
||||
$_SESSION['foo'] = 'baz';
|
||||
$this->storage->clear();
|
||||
|
||||
$this->assertArrayHasKey('foo', $_SESSION);
|
||||
$this->assertSame('baz', $_SESSION['foo']);
|
||||
}
|
||||
}
|
||||
|
@ -129,4 +129,31 @@ class SessionTokenStorageTest extends TestCase
|
||||
|
||||
$this->assertSame('TOKEN', $this->storage->removeToken('token_id'));
|
||||
}
|
||||
|
||||
public function testClearRemovesAllTokensFromTheConfiguredNamespace()
|
||||
{
|
||||
$this->storage->setToken('foo', 'bar');
|
||||
$this->storage->clear();
|
||||
|
||||
$this->assertFalse($this->storage->hasToken('foo'));
|
||||
$this->assertFalse($this->session->has(self::SESSION_NAMESPACE.'/foo'));
|
||||
}
|
||||
|
||||
public function testClearDoesNotRemoveSessionValuesFromOtherNamespaces()
|
||||
{
|
||||
$this->session->set('foo/bar', 'baz');
|
||||
$this->storage->clear();
|
||||
|
||||
$this->assertTrue($this->session->has('foo/bar'));
|
||||
$this->assertSame('baz', $this->session->get('foo/bar'));
|
||||
}
|
||||
|
||||
public function testClearDoesNotRemoveNonNamespacedSessionValues()
|
||||
{
|
||||
$this->session->set('foo', 'baz');
|
||||
$this->storage->clear();
|
||||
|
||||
$this->assertTrue($this->session->has('foo'));
|
||||
$this->assertSame('baz', $this->session->get('foo'));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Csrf\TokenStorage;
|
||||
|
||||
/**
|
||||
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
|
||||
*/
|
||||
interface ClearableTokenStorageInterface extends TokenStorageInterface
|
||||
{
|
||||
/**
|
||||
* Removes all CSRF tokens.
|
||||
*/
|
||||
public function clear();
|
||||
}
|
@ -18,7 +18,7 @@ use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException;
|
||||
*
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
*/
|
||||
class NativeSessionTokenStorage implements TokenStorageInterface
|
||||
class NativeSessionTokenStorage implements ClearableTokenStorageInterface
|
||||
{
|
||||
/**
|
||||
* The namespace used to store values in the session.
|
||||
@ -102,6 +102,14 @@ class NativeSessionTokenStorage implements TokenStorageInterface
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function clear()
|
||||
{
|
||||
unset($_SESSION[$this->namespace]);
|
||||
}
|
||||
|
||||
private function startSession()
|
||||
{
|
||||
if (PHP_SESSION_NONE === session_status()) {
|
||||
|
@ -19,7 +19,7 @@ use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException;
|
||||
*
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
*/
|
||||
class SessionTokenStorage implements TokenStorageInterface
|
||||
class SessionTokenStorage implements ClearableTokenStorageInterface
|
||||
{
|
||||
/**
|
||||
* The namespace used to store values in the session.
|
||||
@ -92,4 +92,16 @@ class SessionTokenStorage implements TokenStorageInterface
|
||||
|
||||
return $this->session->remove($this->namespace.'/'.$tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function clear()
|
||||
{
|
||||
foreach (array_keys($this->session->all()) as $key) {
|
||||
if (0 === strpos($key, $this->namespace.'/')) {
|
||||
$this->session->remove($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Logout;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface;
|
||||
|
||||
/**
|
||||
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
|
||||
*/
|
||||
class CsrfTokenClearingLogoutHandler implements LogoutHandlerInterface
|
||||
{
|
||||
private $csrfTokenStorage;
|
||||
|
||||
public function __construct(ClearableTokenStorageInterface $csrfTokenStorage)
|
||||
{
|
||||
$this->csrfTokenStorage = $csrfTokenStorage;
|
||||
}
|
||||
|
||||
public function logout(Request $request, Response $response, TokenInterface $token)
|
||||
{
|
||||
$this->csrfTokenStorage->clear();
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\Logout;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
||||
use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage;
|
||||
use Symfony\Component\Security\Http\Logout\CsrfTokenClearingLogoutHandler;
|
||||
|
||||
class CsrfTokenClearingLogoutHandlerTest extends TestCase
|
||||
{
|
||||
private $session;
|
||||
private $csrfTokenStorage;
|
||||
private $csrfTokenClearingLogoutHandler;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
$this->session = new Session(new MockArraySessionStorage());
|
||||
$this->csrfTokenStorage = new SessionTokenStorage($this->session, 'foo');
|
||||
$this->csrfTokenStorage->setToken('foo', 'bar');
|
||||
$this->csrfTokenStorage->setToken('foobar', 'baz');
|
||||
$this->csrfTokenClearingLogoutHandler = new CsrfTokenClearingLogoutHandler($this->csrfTokenStorage);
|
||||
}
|
||||
|
||||
public function testCsrfTokenCookieWithSameNamespaceIsRemoved()
|
||||
{
|
||||
$this->assertSame('bar', $this->session->get('foo/foo'));
|
||||
$this->assertSame('baz', $this->session->get('foo/foobar'));
|
||||
|
||||
$this->csrfTokenClearingLogoutHandler->logout(new Request(), new Response(), $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock());
|
||||
|
||||
$this->assertFalse($this->csrfTokenStorage->hasToken('foo'));
|
||||
$this->assertFalse($this->csrfTokenStorage->hasToken('foobar'));
|
||||
|
||||
$this->assertFalse($this->session->has('foo/foo'));
|
||||
$this->assertFalse($this->session->has('foo/foobar'));
|
||||
}
|
||||
|
||||
public function testCsrfTokenCookieWithDifferentNamespaceIsNotRemoved()
|
||||
{
|
||||
$barNamespaceCsrfSessionStorage = new SessionTokenStorage($this->session, 'bar');
|
||||
$barNamespaceCsrfSessionStorage->setToken('foo', 'bar');
|
||||
$barNamespaceCsrfSessionStorage->setToken('foobar', 'baz');
|
||||
|
||||
$this->assertSame('bar', $this->session->get('foo/foo'));
|
||||
$this->assertSame('baz', $this->session->get('foo/foobar'));
|
||||
$this->assertSame('bar', $this->session->get('bar/foo'));
|
||||
$this->assertSame('baz', $this->session->get('bar/foobar'));
|
||||
|
||||
$this->csrfTokenClearingLogoutHandler->logout(new Request(), new Response(), $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock());
|
||||
|
||||
$this->assertTrue($barNamespaceCsrfSessionStorage->hasToken('foo'));
|
||||
$this->assertTrue($barNamespaceCsrfSessionStorage->hasToken('foobar'));
|
||||
$this->assertSame('bar', $barNamespaceCsrfSessionStorage->getToken('foo'));
|
||||
$this->assertSame('baz', $barNamespaceCsrfSessionStorage->getToken('foobar'));
|
||||
$this->assertFalse($this->csrfTokenStorage->hasToken('foo'));
|
||||
$this->assertFalse($this->csrfTokenStorage->hasToken('foobar'));
|
||||
|
||||
$this->assertFalse($this->session->has('foo/foo'));
|
||||
$this->assertFalse($this->session->has('foo/foobar'));
|
||||
$this->assertSame('bar', $this->session->get('bar/foo'));
|
||||
$this->assertSame('baz', $this->session->get('bar/foobar'));
|
||||
}
|
||||
}
|
@ -25,9 +25,12 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/routing": "~3.4|~4.0",
|
||||
"symfony/security-csrf": "~3.4|~4.0",
|
||||
"symfony/security-csrf": "^3.4.11|^4.0.11",
|
||||
"psr/log": "~1.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/security-csrf": ">=3.4.0,<3.4.11 || >=4.0.0,<4.0.11"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/security-csrf": "For using tokens to protect authentication/logout attempts",
|
||||
"symfony/routing": "For using the HttpUtils class to create sub-requests, redirect the user, and match URLs"
|
||||
|
Reference in New Issue
Block a user