Merge branch '2.7' into 2.8
* 2.7: clear CSRF tokens when the user is logged out
This commit is contained in:
commit
a5d0b571fa
@ -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;
|
namespace Symfony\Bundle\SecurityBundle;
|
||||||
|
|
||||||
|
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfTokenClearingLogoutHandlerPass;
|
||||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||||
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
|
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
@ -58,5 +59,6 @@ class SecurityBundle extends Bundle
|
|||||||
$extension->addUserProviderFactory(new LdapFactory());
|
$extension->addUserProviderFactory(new LdapFactory());
|
||||||
$container->addCompilerPass(new AddSecurityVotersPass());
|
$container->addCompilerPass(new AddSecurityVotersPass());
|
||||||
$container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_AFTER_REMOVING);
|
$container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_AFTER_REMOVING);
|
||||||
|
$container->addCompilerPass(new RegisterCsrfTokenClearingLogoutHandlerPass());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,4 +31,22 @@ class LogoutTest extends WebTestCase
|
|||||||
|
|
||||||
$this->assertNull($cookieJar->get('REMEMBERME'));
|
$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
|
@ -18,7 +18,7 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": ">=5.3.9",
|
"php": ">=5.3.9",
|
||||||
"ext-xml": "*",
|
"ext-xml": "*",
|
||||||
"symfony/security": "^2.8.40|~3.4.10",
|
"symfony/security": "^2.8.41|~3.4.11",
|
||||||
"symfony/security-acl": "~2.7|~3.0.0",
|
"symfony/security-acl": "~2.7|~3.0.0",
|
||||||
"symfony/http-kernel": "~2.7|~3.0.0",
|
"symfony/http-kernel": "~2.7|~3.0.0",
|
||||||
"symfony/polyfill-php70": "~1.0"
|
"symfony/polyfill-php70": "~1.0"
|
||||||
|
@ -116,4 +116,32 @@ class NativeSessionTokenStorageTest extends TestCase
|
|||||||
$this->assertSame('TOKEN', $this->storage->removeToken('token_id'));
|
$this->assertSame('TOKEN', $this->storage->removeToken('token_id'));
|
||||||
$this->assertFalse($this->storage->hasToken('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'));
|
$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>
|
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||||
*/
|
*/
|
||||||
class NativeSessionTokenStorage implements TokenStorageInterface
|
class NativeSessionTokenStorage implements ClearableTokenStorageInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The namespace used to store values in the session.
|
* The namespace used to store values in the session.
|
||||||
@ -96,6 +96,14 @@ class NativeSessionTokenStorage implements TokenStorageInterface
|
|||||||
return $token;
|
return $token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function clear()
|
||||||
|
{
|
||||||
|
unset($_SESSION[$this->namespace]);
|
||||||
|
}
|
||||||
|
|
||||||
private function startSession()
|
private function startSession()
|
||||||
{
|
{
|
||||||
if (\PHP_VERSION_ID >= 50400) {
|
if (\PHP_VERSION_ID >= 50400) {
|
||||||
|
@ -19,7 +19,7 @@ use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException;
|
|||||||
*
|
*
|
||||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||||
*/
|
*/
|
||||||
class SessionTokenStorage implements TokenStorageInterface
|
class SessionTokenStorage implements ClearableTokenStorageInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The namespace used to store values in the session.
|
* The namespace used to store values in the session.
|
||||||
@ -92,4 +92,16 @@ class SessionTokenStorage implements TokenStorageInterface
|
|||||||
|
|
||||||
return $this->session->remove($this->namespace.'/'.$tokenId);
|
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'));
|
||||||
|
}
|
||||||
|
}
|
@ -27,9 +27,12 @@
|
|||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"symfony/routing": "~2.2|~3.0.0",
|
"symfony/routing": "~2.2|~3.0.0",
|
||||||
"symfony/security-csrf": "^2.8.31|^3.3.13",
|
"symfony/security-csrf": "^2.8.41|^3.3.17",
|
||||||
"psr/log": "~1.0"
|
"psr/log": "~1.0"
|
||||||
},
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/security-csrf": "<2.7.48 || >=2.8.0,<2.8.41 || >=3.0.0"
|
||||||
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"symfony/security-csrf": "For using tokens to protect authentication/logout attempts",
|
"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"
|
"symfony/routing": "For using the HttpUtils class to create sub-requests, redirect the user, and match URLs"
|
||||||
|
Reference in New Issue
Block a user