diff options
-rw-r--r-- | CHANGELOG.md | 3 | ||||
-rw-r--r-- | Csrf/.gitignore | 3 | ||||
-rw-r--r-- | Csrf/CsrfTokenGenerator.php | 105 | ||||
-rw-r--r-- | Csrf/CsrfTokenGeneratorInterface.php | 52 | ||||
-rw-r--r-- | Csrf/LICENSE | 19 | ||||
-rw-r--r-- | Csrf/README.md | 21 | ||||
-rw-r--r-- | Csrf/Tests/CsrfTokenGeneratorTest.php | 148 | ||||
-rw-r--r-- | Csrf/Tests/TokenStorage/NativeSessionTokenStorageTest.php | 99 | ||||
-rw-r--r-- | Csrf/Tests/TokenStorage/SessionTokenStorageTest.php | 144 | ||||
-rw-r--r-- | Csrf/TokenStorage/NativeSessionTokenStorage.php | 101 | ||||
-rw-r--r-- | Csrf/TokenStorage/SessionTokenStorage.php | 89 | ||||
-rw-r--r-- | Csrf/TokenStorage/TokenStorageInterface.php | 49 | ||||
-rw-r--r-- | Csrf/composer.json | 38 | ||||
-rw-r--r-- | Csrf/phpunit.xml.dist | 29 | ||||
-rw-r--r-- | Http/Firewall/LogoutListener.php | 22 | ||||
-rw-r--r-- | Http/Firewall/SimpleFormAuthenticationListener.php | 15 | ||||
-rw-r--r-- | Http/Firewall/UsernamePasswordFormAuthenticationListener.php | 12 | ||||
-rw-r--r-- | Http/composer.json | 3 | ||||
-rw-r--r-- | composer.json | 1 |
19 files changed, 928 insertions, 25 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 6802055..9ab61d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ CHANGELOG * The switch user listener now preserves the query string when switching a user * The remember-me cookie hashes now use HMAC, which means that current cookies will be invalidated * added simpler customization options + * structured component into three sub-components Acl, Core and Http + * added Csrf sub-component + * changed Http sub-component to depend on Csrf sub-component instead of the Form component 2.3.0 ----- diff --git a/Csrf/.gitignore b/Csrf/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/Csrf/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/Csrf/CsrfTokenGenerator.php b/Csrf/CsrfTokenGenerator.php new file mode 100644 index 0000000..8ff3462 --- /dev/null +++ b/Csrf/CsrfTokenGenerator.php @@ -0,0 +1,105 @@ +<?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; + +use Symfony\Component\Security\Core\Util\SecureRandomInterface; +use Symfony\Component\Security\Core\Util\SecureRandom; +use Symfony\Component\Security\Core\Util\StringUtils; +use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage; +use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface; + +/** + * Generates and validates CSRF tokens. + * + * @since 2.4 + * @author Bernhard Schussek <bernhard.schussek@symfony.com> + */ +class CsrfTokenGenerator implements CsrfTokenGeneratorInterface +{ + /** + * The entropy of the token in bits. + * @var integer + */ + const TOKEN_ENTROPY = 256; + + /** + * @var TokenStorageInterface + */ + private $storage; + + /** + * The generator for random values. + * @var SecureRandomInterface + */ + private $random; + + /** + * Creates a new CSRF provider using PHP's native session storage. + * + * @param TokenStorageInterface $storage The storage for storing generated + * CSRF tokens + * @param SecureRandomInterface $random The used random value generator + * @param integer $entropy The amount of entropy collected for + * newly generated tokens (in bits) + * + */ + public function __construct(TokenStorageInterface $storage = null, SecureRandomInterface $random = null, $entropy = self::TOKEN_ENTROPY) + { + if (null === $storage) { + $storage = new NativeSessionTokenStorage(); + } + + if (null === $random) { + $random = new SecureRandom(); + } + + $this->storage = $storage; + $this->random = $random; + $this->entropy = $entropy; + } + + /** + * {@inheritDoc} + */ + public function generateCsrfToken($tokenId) + { + $currentToken = $this->storage->getToken($tokenId, false); + + // Token exists and is still valid + if (false !== $currentToken) { + return $currentToken; + } + + // Token needs to be (re)generated + // Generate an URI safe base64 encoded string that does not contain "+", + // "/" or "=" which need to be URL encoded and make URLs unnecessarily + // longer. + $bytes = $this->random->nextBytes($this->entropy / 8); + $token = rtrim(strtr(base64_encode($bytes), '+/', '-_'), '='); + + $this->storage->setToken($tokenId, $token); + + return $token; + } + + /** + * {@inheritDoc} + */ + public function isCsrfTokenValid($tokenId, $token) + { + if (!$this->storage->hasToken($tokenId)) { + return false; + } + + return StringUtils::equals((string) $this->storage->getToken($tokenId), $token); + } +} diff --git a/Csrf/CsrfTokenGeneratorInterface.php b/Csrf/CsrfTokenGeneratorInterface.php new file mode 100644 index 0000000..c34549f --- /dev/null +++ b/Csrf/CsrfTokenGeneratorInterface.php @@ -0,0 +1,52 @@ +<?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; + +/** + * Generates and validates CSRF tokens. + * + * You can generate a CSRF token by using the method {@link generateCsrfToken()}. + * This method expects a unique token ID as argument. The token ID can later be + * used to validate a token provided by the user. + * + * Token IDs do not necessarily have to be secret, but they should NEVER be + * created from data provided by the client. A good practice is to hard-code the + * token IDs for the various CSRF tokens used by your application. + * + * You should use the method {@link isCsrfTokenValid()} to check a CSRF token + * submitted by the client. This method will return true if the CSRF token is + * valid. + * + * @since 2.4 + * @author Bernhard Schussek <bschussek@gmail.com> + */ +interface CsrfTokenGeneratorInterface +{ + /** + * Generates a CSRF token with the given token ID. + * + * @param string $tokenId An ID that identifies the token + * + * @return string The generated CSRF token + */ + public function generateCsrfToken($tokenId); + + /** + * Validates a CSRF token. + * + * @param string $tokenId The token ID used when generating the token + * @param string $token The token supplied by the client + * + * @return Boolean Whether the token supplied by the client is correct + */ + public function isCsrfTokenValid($tokenId, $token); +} diff --git a/Csrf/LICENSE b/Csrf/LICENSE new file mode 100644 index 0000000..88a57f8 --- /dev/null +++ b/Csrf/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2013 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Csrf/README.md b/Csrf/README.md new file mode 100644 index 0000000..394a29e --- /dev/null +++ b/Csrf/README.md @@ -0,0 +1,21 @@ +Security Component - CSRF +========================= + +The Security CSRF (cross-site request forgery) component provides a class +`CsrfTokenGenerator` for generating and validating CSRF tokens. + +Resources +--------- + +Documentation: + +http://symfony.com/doc/2.4/book/security.html + +Tests +----- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/Security/Csrf/ + $ composer.phar install --dev + $ phpunit diff --git a/Csrf/Tests/CsrfTokenGeneratorTest.php b/Csrf/Tests/CsrfTokenGeneratorTest.php new file mode 100644 index 0000000..f5f9507 --- /dev/null +++ b/Csrf/Tests/CsrfTokenGeneratorTest.php @@ -0,0 +1,148 @@ +<?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\Form\Tests\Extension\Csrf\CsrfProvider; + +use Symfony\Component\Security\Csrf\CsrfTokenGenerator; + +/** + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class CsrfTokenGeneratorTest extends \PHPUnit_Framework_TestCase +{ + /** + * A non alpha-numeric byte string + * @var string + */ + private static $bytes; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $random; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $storage; + + /** + * @var CsrfTokenGenerator + */ + private $generator; + + public static function setUpBeforeClass() + { + self::$bytes = base64_decode('aMf+Tct/RLn2WQ=='); + } + + protected function setUp() + { + $this->random = $this->getMock('Symfony\Component\Security\Core\Util\SecureRandomInterface'); + $this->storage = $this->getMock('Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface'); + $this->generator = new CsrfTokenGenerator($this->storage, $this->random); + } + + protected function tearDown() + { + $this->random = null; + $this->storage = null; + $this->generator = null; + } + + public function testGenerateNewToken() + { + $this->storage->expects($this->once()) + ->method('getToken') + ->with('token_id', false) + ->will($this->returnValue(false)); + + $this->storage->expects($this->once()) + ->method('setToken') + ->with('token_id', $this->anything()) + ->will($this->returnCallback(function ($tokenId, $token) use (&$storedToken) { + $storedToken = $token; + })); + + $this->random->expects($this->once()) + ->method('nextBytes') + ->will($this->returnValue(self::$bytes)); + + $token = $this->generator->generateCsrfToken('token_id'); + + $this->assertSame($token, $storedToken); + $this->assertTrue(ctype_print($token), 'is printable'); + $this->assertStringNotMatchesFormat('%S+%S', $token, 'is URI safe'); + $this->assertStringNotMatchesFormat('%S/%S', $token, 'is URI safe'); + $this->assertStringNotMatchesFormat('%S=%S', $token, 'is URI safe'); + } + + public function testUseExistingTokenIfAvailable() + { + $this->storage->expects($this->once()) + ->method('getToken') + ->with('token_id', false) + ->will($this->returnValue('TOKEN')); + + $this->storage->expects($this->never()) + ->method('setToken'); + + $this->random->expects($this->never()) + ->method('nextBytes'); + + $token = $this->generator->generateCsrfToken('token_id'); + + $this->assertEquals('TOKEN', $token); + } + + public function testMatchingTokenIsValid() + { + $this->storage->expects($this->once()) + ->method('hasToken') + ->with('token_id') + ->will($this->returnValue(true)); + + $this->storage->expects($this->once()) + ->method('getToken') + ->with('token_id') + ->will($this->returnValue('TOKEN')); + + $this->assertTrue($this->generator->isCsrfTokenValid('token_id', 'TOKEN')); + } + + public function testNonMatchingTokenIsNotValid() + { + $this->storage->expects($this->once()) + ->method('hasToken') + ->with('token_id') + ->will($this->returnValue(true)); + + $this->storage->expects($this->once()) + ->method('getToken') + ->with('token_id') + ->will($this->returnValue('TOKEN')); + + $this->assertFalse($this->generator->isCsrfTokenValid('token_id', 'FOOBAR')); + } + + public function testNonExistingTokenIsNotValid() + { + $this->storage->expects($this->once()) + ->method('hasToken') + ->with('token_id') + ->will($this->returnValue(false)); + + $this->storage->expects($this->never()) + ->method('getToken'); + + $this->assertFalse($this->generator->isCsrfTokenValid('token_id', 'FOOBAR')); + } +} diff --git a/Csrf/Tests/TokenStorage/NativeSessionTokenStorageTest.php b/Csrf/Tests/TokenStorage/NativeSessionTokenStorageTest.php new file mode 100644 index 0000000..69df061 --- /dev/null +++ b/Csrf/Tests/TokenStorage/NativeSessionTokenStorageTest.php @@ -0,0 +1,99 @@ +<?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\Form\Tests\Extension\Csrf\CsrfProvider; + +use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage; + +/** + * @author Bernhard Schussek <bschussek@gmail.com> + * + * @runTestsInSeparateProcesses + */ +class NativeSessionTokenStorageTest extends \PHPUnit_Framework_TestCase +{ + const SESSION_NAMESPACE = 'foobar'; + + /** + * @var NativeSessionTokenStorage + */ + private $storage; + + public static function setUpBeforeClass() + { + ini_set('session.save_handler', 'files'); + ini_set('session.save_path', sys_get_temp_dir()); + + parent::setUpBeforeClass(); + } + + protected function setUp() + { + $_SESSION = array(); + + $this->storage = new NativeSessionTokenStorage(self::SESSION_NAMESPACE); + } + + public function testStoreTokenInClosedSession() + { + $this->storage->setToken('token_id', 'TOKEN'); + + $this->assertSame(array(self::SESSION_NAMESPACE => array('token_id' => 'TOKEN')), $_SESSION); + } + + public function testStoreTokenInClosedSessionWithExistingSessionId() + { + session_id('foobar'); + + $this->assertSame(PHP_SESSION_NONE, session_status()); + + $this->storage->setToken('token_id', 'TOKEN'); + + $this->assertSame(PHP_SESSION_ACTIVE, session_status()); + $this->assertSame(array(self::SESSION_NAMESPACE => array('token_id' => 'TOKEN')), $_SESSION); + } + + public function testStoreTokenInActiveSession() + { + session_start(); + + $this->storage->setToken('token_id', 'TOKEN'); + + $this->assertSame(array(self::SESSION_NAMESPACE => array('token_id' => 'TOKEN')), $_SESSION); + } + + /** + * @depends testStoreTokenInClosedSession + */ + public function testCheckToken() + { + $this->assertFalse($this->storage->hasToken('token_id')); + + $this->storage->setToken('token_id', 'TOKEN'); + + $this->assertTrue($this->storage->hasToken('token_id')); + } + + /** + * @depends testStoreTokenInClosedSession + */ + public function testGetExistingToken() + { + $this->storage->setToken('token_id', 'TOKEN'); + + $this->assertSame('TOKEN', $this->storage->getToken('token_id')); + } + + public function testGetNonExistingToken() + { + $this->assertSame('DEFAULT', $this->storage->getToken('token_id', 'DEFAULT')); + } +} diff --git a/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php b/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php new file mode 100644 index 0000000..5c8c173 --- /dev/null +++ b/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php @@ -0,0 +1,144 @@ +<?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\Form\Tests\Extension\Csrf\CsrfProvider; + +use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage; + +/** + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class SessionTokenStorageTest extends \PHPUnit_Framework_TestCase +{ + const SESSION_NAMESPACE = 'foobar'; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $session; + + /** + * @var SessionTokenStorage + */ + private $storage; + + protected function setUp() + { + if (!class_exists('Symfony\Component\HttpFoundation\Session\SessionInterface')) { + $this->markTestSkipped('The "HttpFoundation" component is not available'); + } + + $this->session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface') + ->disableOriginalConstructor() + ->getMock(); + $this->storage = new SessionTokenStorage($this->session, self::SESSION_NAMESPACE); + } + + public function testStoreTokenInClosedSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(false)); + + $this->session->expects($this->once()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('set') + ->with(self::SESSION_NAMESPACE.'/token_id', 'TOKEN'); + + $this->storage->setToken('token_id', 'TOKEN'); + } + + public function testStoreTokenInActiveSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(true)); + + $this->session->expects($this->never()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('set') + ->with(self::SESSION_NAMESPACE.'/token_id', 'TOKEN'); + + $this->storage->setToken('token_id', 'TOKEN'); + } + + public function testCheckTokenInClosedSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(false)); + + $this->session->expects($this->once()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('has') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->storage->hasToken('token_id')); + } + + public function testCheckTokenInActiveSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(true)); + + $this->session->expects($this->never()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('has') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->storage->hasToken('token_id')); + } + + public function testGetTokenFromClosedSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(false)); + + $this->session->expects($this->once()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('get') + ->with(self::SESSION_NAMESPACE.'/token_id', 'DEFAULT') + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->storage->getToken('token_id', 'DEFAULT')); + } + + public function testGetTokenFromActiveSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(true)); + + $this->session->expects($this->never()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('get') + ->with(self::SESSION_NAMESPACE.'/token_id', 'DEFAULT') + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->storage->getToken('token_id', 'DEFAULT')); + } +} diff --git a/Csrf/TokenStorage/NativeSessionTokenStorage.php b/Csrf/TokenStorage/NativeSessionTokenStorage.php new file mode 100644 index 0000000..8956743 --- /dev/null +++ b/Csrf/TokenStorage/NativeSessionTokenStorage.php @@ -0,0 +1,101 @@ +<?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; + +/** + * Token storage that uses PHP's native session handling. + * + * @since 2.4 + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class NativeSessionTokenStorage implements TokenStorageInterface +{ + /** + * The namespace used to store values in the session. + * @var string + */ + const SESSION_NAMESPACE = '_csrf'; + + /** + * @var Boolean + */ + private $sessionStarted = false; + + /** + * @var string + */ + private $namespace; + + /** + * Initializes the storage with a session namespace. + * + * @param string $namespace The namespace under which the token is stored + * in the session + */ + public function __construct($namespace = self::SESSION_NAMESPACE) + { + $this->namespace = $namespace; + } + + /** + * {@inheritdoc} + */ + public function getToken($tokenId, $default = null) + { + if (!$this->sessionStarted) { + $this->startSession(); + } + + if (isset($_SESSION[$this->namespace][$tokenId])) { + return $_SESSION[$this->namespace][$tokenId]; + } + + return $default; + } + + /** + * {@inheritdoc} + */ + public function setToken($tokenId, $token) + { + if (!$this->sessionStarted) { + $this->startSession(); + } + + $_SESSION[$this->namespace][$tokenId] = $token; + } + + /** + * {@inheritdoc} + */ + public function hasToken($tokenId) + { + if (!$this->sessionStarted) { + $this->startSession(); + } + + return isset($_SESSION[$this->namespace][$tokenId]); + } + + private function startSession() + { + if (version_compare(PHP_VERSION, '5.4', '>=')) { + if (PHP_SESSION_NONE === session_status()) { + session_start(); + } + } elseif (!session_id()) { + session_start(); + } + + $this->sessionStarted = true; + } +} diff --git a/Csrf/TokenStorage/SessionTokenStorage.php b/Csrf/TokenStorage/SessionTokenStorage.php new file mode 100644 index 0000000..3878e4c --- /dev/null +++ b/Csrf/TokenStorage/SessionTokenStorage.php @@ -0,0 +1,89 @@ +<?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; + +use Symfony\Component\HttpFoundation\Session\SessionInterface; + +/** + * Token storage that uses a Symfony2 Session object. + * + * @since 2.4 + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class SessionTokenStorage implements TokenStorageInterface +{ + /** + * The namespace used to store values in the session. + * @var string + */ + const SESSION_NAMESPACE = '_csrf'; + + /** + * The user session from which the session ID is returned + * @var SessionInterface + */ + private $session; + + /** + * @var string + */ + private $namespace; + + /** + * Initializes the storage with a Session object and a session namespace. + * + * @param SessionInterface $session The user session + * @param string $namespace The namespace under which the token + * is stored in the session + */ + public function __construct(SessionInterface $session, $namespace = self::SESSION_NAMESPACE) + { + $this->session = $session; + $this->namespace = $namespace; + } + + /** + * {@inheritdoc} + */ + public function getToken($tokenId, $default = null) + { + if (!$this->session->isStarted()) { + $this->session->start(); + } + + return $this->session->get($this->namespace . '/' . $tokenId, $default); + } + + /** + * {@inheritdoc} + */ + public function setToken($tokenId, $token) + { + if (!$this->session->isStarted()) { + $this->session->start(); + } + + $this->session->set($this->namespace . '/' . $tokenId, $token); + } + + /** + * {@inheritdoc} + */ + public function hasToken($tokenId) + { + if (!$this->session->isStarted()) { + $this->session->start(); + } + + return $this->session->has($this->namespace . '/' . $tokenId); + } +} diff --git a/Csrf/TokenStorage/TokenStorageInterface.php b/Csrf/TokenStorage/TokenStorageInterface.php new file mode 100644 index 0000000..7dba9e5 --- /dev/null +++ b/Csrf/TokenStorage/TokenStorageInterface.php @@ -0,0 +1,49 @@ +<?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; + +/** + * Stores CSRF tokens. + * + * @since 2.4 + * @author Bernhard Schussek <bschussek@gmail.com> + */ +interface TokenStorageInterface +{ + /** + * Reads a stored CSRF token. + * + * @param string $tokenId The token ID + * @param mixed $default The value to be returned if no token is set + * + * @return mixed The stored token or the default value, if no token is set + */ + public function getToken($tokenId, $default = null); + + /** + * Stores a CSRF token. + * + * @param string $tokenId The token ID + * @param mixed $token The CSRF token + */ + public function setToken($tokenId, $token); + + /** + * Checks whether a token with the given token ID exists. + * + * @param string $tokenId The token ID + * + * @return Boolean Returns true if a token is stored for the given token ID, + * false otherwise. + */ + public function hasToken($tokenId); +} diff --git a/Csrf/composer.json b/Csrf/composer.json new file mode 100644 index 0000000..3cfc2b4 --- /dev/null +++ b/Csrf/composer.json @@ -0,0 +1,38 @@ +{ + "name": "symfony/security-csrf", + "type": "library", + "description": "Symfony Security Component - CSRF Library", + "keywords": [], + "homepage": "http://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.3", + "symfony/security-core": "~2.4" + }, + "require-dev": { + "symfony/http-foundation": "~2.1" + }, + "suggest": { + "symfony/http-foundation": "For using the class SessionTokenStorage." + }, + "autoload": { + "psr-0": { "Symfony\\Component\\Security\\Csrf\\": "" } + }, + "target-dir": "Symfony/Component/Security/Csrf", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + } +} diff --git a/Csrf/phpunit.xml.dist b/Csrf/phpunit.xml.dist new file mode 100644 index 0000000..0718c76 --- /dev/null +++ b/Csrf/phpunit.xml.dist @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<phpunit backupGlobals="false" + backupStaticAttributes="false" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + processIsolation="false" + stopOnFailure="false" + syntaxCheck="false" + bootstrap="vendor/autoload.php" +> + <testsuites> + <testsuite name="Symfony Security Component CSRF Test Suite"> + <directory>./Tests/</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist> + <directory>./</directory> + <exclude> + <directory>./vendor</directory> + <directory>./Tests</directory> + </exclude> + </whitelist> + </filter> +</phpunit> diff --git a/Http/Firewall/LogoutListener.php b/Http/Firewall/LogoutListener.php index 983eab0..073a48c 100644 --- a/Http/Firewall/LogoutListener.php +++ b/Http/Firewall/LogoutListener.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Security\Http\Firewall; -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\LogoutException; +use Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface; use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface; @@ -34,18 +34,18 @@ class LogoutListener implements ListenerInterface private $handlers; private $successHandler; private $httpUtils; - private $csrfProvider; + private $csrfTokenGenerator; /** * Constructor. * * @param SecurityContextInterface $securityContext - * @param HttpUtils $httpUtils An HttpUtilsInterface instance - * @param LogoutSuccessHandlerInterface $successHandler A LogoutSuccessHandlerInterface instance - * @param array $options An array of options to process a logout attempt - * @param CsrfProviderInterface $csrfProvider A CsrfProviderInterface instance + * @param HttpUtils $httpUtils An HttpUtilsInterface instance + * @param LogoutSuccessHandlerInterface $successHandler A LogoutSuccessHandlerInterface instance + * @param array $options An array of options to process a logout attempt + * @param CsrfTokenGeneratorInterface $csrfTokenGenerator A CsrfTokenGeneratorInterface instance */ - public function __construct(SecurityContextInterface $securityContext, HttpUtils $httpUtils, LogoutSuccessHandlerInterface $successHandler, array $options = array(), CsrfProviderInterface $csrfProvider = null) + public function __construct(SecurityContextInterface $securityContext, HttpUtils $httpUtils, LogoutSuccessHandlerInterface $successHandler, array $options = array(), CsrfTokenGeneratorInterface $csrfTokenGenerator = null) { $this->securityContext = $securityContext; $this->httpUtils = $httpUtils; @@ -55,7 +55,7 @@ class LogoutListener implements ListenerInterface 'logout_path' => '/logout', ), $options); $this->successHandler = $successHandler; - $this->csrfProvider = $csrfProvider; + $this->csrfTokenGenerator = $csrfTokenGenerator; $this->handlers = array(); } @@ -72,7 +72,7 @@ class LogoutListener implements ListenerInterface /** * Performs the logout if requested * - * If a CsrfProviderInterface instance is available, it will be used to + * If a CsrfTokenGeneratorInterface instance is available, it will be used to * validate the request. * * @param GetResponseEvent $event A GetResponseEvent instance @@ -88,10 +88,10 @@ class LogoutListener implements ListenerInterface return; } - if (null !== $this->csrfProvider) { + if (null !== $this->csrfTokenGenerator) { $csrfToken = $request->get($this->options['csrf_parameter'], null, true); - if (false === $this->csrfProvider->isCsrfTokenValid($this->options['intention'], $csrfToken)) { + if (false === $this->csrfTokenGenerator->isCsrfTokenValid($this->options['intention'], $csrfToken)) { throw new LogoutException('Invalid CSRF token.'); } } diff --git a/Http/Firewall/SimpleFormAuthenticationListener.php b/Http/Firewall/SimpleFormAuthenticationListener.php index 054616b..c09cbdb 100644 --- a/Http/Firewall/SimpleFormAuthenticationListener.php +++ b/Http/Firewall/SimpleFormAuthenticationListener.php @@ -13,10 +13,11 @@ namespace Symfony\Component\Security\Http\Firewall; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; +use Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; -use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; use Symfony\Component\Security\Core\Authentication\SimpleFormAuthenticatorInterface; use Symfony\Component\Security\Core\SecurityContextInterface; use Symfony\Component\Security\Http\HttpUtils; @@ -29,7 +30,7 @@ use Psr\Log\LoggerInterface; class SimpleFormAuthenticationListener extends AbstractAuthenticationListener { private $simpleAuthenticator; - private $csrfProvider; + private $csrfTokenGenerator; /** * Constructor. @@ -46,16 +47,16 @@ class SimpleFormAuthenticationListener extends AbstractAuthenticationListener * @param LoggerInterface $logger A LoggerInterface instance * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance * @param SimpleFormAuthenticatorInterface $simpleAuthenticator A SimpleFormAuthenticatorInterface instance - * @param CsrfProviderInterface $csrfProvider A CsrfProviderInterface instance + * @param CsrfTokenGeneratorInterface $csrfTokenGenerator A CsrfTokenGeneratorInterface instance */ - public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfProviderInterface $csrfProvider = null, SimpleFormAuthenticatorInterface $simpleAuthenticator = null) + public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfTokenGeneratorInterface $csrfTokenGenerator = null, SimpleFormAuthenticatorInterface $simpleAuthenticator = null) { if (!$simpleAuthenticator) { throw new \InvalidArgumentException('Missing simple authenticator'); } $this->simpleAuthenticator = $simpleAuthenticator; - $this->csrfProvider = $csrfProvider; + $this->csrfTokenGenerator = $csrfTokenGenerator; $options = array_merge(array( 'username_parameter' => '_username', @@ -84,10 +85,10 @@ class SimpleFormAuthenticationListener extends AbstractAuthenticationListener */ protected function attemptAuthentication(Request $request) { - if (null !== $this->csrfProvider) { + if (null !== $this->csrfTokenGenerator) { $csrfToken = $request->get($this->options['csrf_parameter'], null, true); - if (false === $this->csrfProvider->isCsrfTokenValid($this->options['intention'], $csrfToken)) { + if (false === $this->csrfTokenGenerator->isCsrfTokenValid($this->options['intention'], $csrfToken)) { throw new InvalidCsrfTokenException('Invalid CSRF token.'); } } diff --git a/Http/Firewall/UsernamePasswordFormAuthenticationListener.php b/Http/Firewall/UsernamePasswordFormAuthenticationListener.php index 81c2b37..7c42dec 100644 --- a/Http/Firewall/UsernamePasswordFormAuthenticationListener.php +++ b/Http/Firewall/UsernamePasswordFormAuthenticationListener.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Security\Http\Firewall; -use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; use Symfony\Component\HttpFoundation\Request; use Psr\Log\LoggerInterface; +use Symfony\Component\Security\Csrf\CsrfTokenGeneratorInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; @@ -32,12 +32,12 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; */ class UsernamePasswordFormAuthenticationListener extends AbstractAuthenticationListener { - private $csrfProvider; + private $csrfTokenGenerator; /** * {@inheritdoc} */ - public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfProviderInterface $csrfProvider = null) + public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfTokenGeneratorInterface $csrfTokenGenerator = null) { parent::__construct($securityContext, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array( 'username_parameter' => '_username', @@ -47,7 +47,7 @@ class UsernamePasswordFormAuthenticationListener extends AbstractAuthenticationL 'post_only' => true, ), $options), $logger, $dispatcher); - $this->csrfProvider = $csrfProvider; + $this->csrfTokenGenerator = $csrfTokenGenerator; } /** @@ -67,10 +67,10 @@ class UsernamePasswordFormAuthenticationListener extends AbstractAuthenticationL */ protected function attemptAuthentication(Request $request) { - if (null !== $this->csrfProvider) { + if (null !== $this->csrfTokenGenerator) { $csrfToken = $request->get($this->options['csrf_parameter'], null, true); - if (false === $this->csrfProvider->isCsrfTokenValid($this->options['intention'], $csrfToken)) { + if (false === $this->csrfTokenGenerator->isCsrfTokenValid($this->options['intention'], $csrfToken)) { throw new InvalidCsrfTokenException('Invalid CSRF token.'); } } diff --git a/Http/composer.json b/Http/composer.json index 6b610a0..4dfd985 100644 --- a/Http/composer.json +++ b/Http/composer.json @@ -25,10 +25,11 @@ "require-dev": { "symfony/form": "~2.0", "symfony/routing": "~2.2", + "symfony/security-csrf": "~2.4", "psr/log": "~1.0" }, "suggest": { - "symfony/form": "", + "symfony/security-csrf": "", "symfony/routing": "" }, "autoload": { diff --git a/composer.json b/composer.json index 164deb3..6be3886 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "replace": { "symfony/security-acl": "self.version", "symfony/security-core": "self.version", + "symfony/security-csrf": "self.version", "symfony/security-http": "self.version" }, "require-dev": { |