diff options
Diffstat (limited to 'Csrf')
-rw-r--r-- | Csrf/CsrfToken.php | 66 | ||||
-rw-r--r-- | Csrf/CsrfTokenGenerator.php | 105 | ||||
-rw-r--r-- | Csrf/CsrfTokenManager.php | 106 | ||||
-rw-r--r-- | Csrf/CsrfTokenManagerInterface.php | 67 | ||||
-rw-r--r-- | Csrf/Exception/TokenNotFoundException.php | 21 | ||||
-rw-r--r-- | Csrf/README.md | 2 | ||||
-rw-r--r-- | Csrf/Tests/CsrfTokenGeneratorTest.php | 148 | ||||
-rw-r--r-- | Csrf/Tests/CsrfTokenManagerTest.php | 164 | ||||
-rw-r--r-- | Csrf/Tests/TokenGenerator/UriSafeTokenGeneratorTest.php | 71 | ||||
-rw-r--r-- | Csrf/Tests/TokenStorage/NativeSessionTokenStorageTest.php | 25 | ||||
-rw-r--r-- | Csrf/Tests/TokenStorage/SessionTokenStorageTest.php | 130 | ||||
-rw-r--r-- | Csrf/TokenGenerator/TokenGeneratorInterface.php (renamed from Csrf/CsrfTokenGeneratorInterface.php) | 20 | ||||
-rw-r--r-- | Csrf/TokenGenerator/UriSafeTokenGenerator.php | 73 | ||||
-rw-r--r-- | Csrf/TokenStorage/NativeSessionTokenStorage.php | 30 | ||||
-rw-r--r-- | Csrf/TokenStorage/SessionTokenStorage.php | 25 | ||||
-rw-r--r-- | Csrf/TokenStorage/TokenStorageInterface.php | 22 |
16 files changed, 783 insertions, 292 deletions
diff --git a/Csrf/CsrfToken.php b/Csrf/CsrfToken.php new file mode 100644 index 0000000..aa3da45 --- /dev/null +++ b/Csrf/CsrfToken.php @@ -0,0 +1,66 @@ +<?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; + +/** + * A CSRF token. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class CsrfToken +{ + /** + * @var string + */ + private $id; + + /** + * @var string + */ + private $value; + + public function __construct($id, $value) + { + $this->id = (string) $id; + $this->value = (string) $value; + } + + /** + * Returns the ID of the CSRF token. + * + * @return string The token ID + */ + public function getId() + { + return $this->id; + } + + /** + * Returns the value of the CSRF token. + * + * @return string The token value + */ + public function getValue() + { + return $this->value; + } + + /** + * Returns the value of the CSRF token. + * + * @return string The token value. + */ + public function __toString() + { + return $this->value; + } +} diff --git a/Csrf/CsrfTokenGenerator.php b/Csrf/CsrfTokenGenerator.php deleted file mode 100644 index 8ff3462..0000000 --- a/Csrf/CsrfTokenGenerator.php +++ /dev/null @@ -1,105 +0,0 @@ -<?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/CsrfTokenManager.php b/Csrf/CsrfTokenManager.php new file mode 100644 index 0000000..fa6e19e --- /dev/null +++ b/Csrf/CsrfTokenManager.php @@ -0,0 +1,106 @@ +<?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\StringUtils; +use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator; +use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface; +use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage; +use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface; + +/** + * Default implementation of {@link CsrfTokenManagerInterface}. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class CsrfTokenManager implements CsrfTokenManagerInterface +{ + /** + * @var TokenGeneratorInterface + */ + private $generator; + + /** + * @var TokenStorageInterface + */ + private $storage; + + /** + * Creates a new CSRF provider using PHP's native session storage. + * + * @param TokenGeneratorInterface $generator The token generator + * @param TokenStorageInterface $storage The storage for storing + * generated CSRF tokens + * + */ + public function __construct(TokenGeneratorInterface $generator = null, TokenStorageInterface $storage = null) + { + if (null === $generator) { + $generator = new UriSafeTokenGenerator(); + } + + if (null === $storage) { + $storage = new NativeSessionTokenStorage(); + } + + $this->generator = $generator; + $this->storage = $storage; + } + + /** + * {@inheritdoc} + */ + public function getToken($tokenId) + { + if ($this->storage->hasToken($tokenId)) { + $value = $this->storage->getToken($tokenId); + } else { + $value = $this->generator->generateToken(); + + $this->storage->setToken($tokenId, $value); + } + + return new CsrfToken($tokenId, $value); + } + + /** + * {@inheritdoc} + */ + public function refreshToken($tokenId) + { + $value = $this->generator->generateToken(); + + $this->storage->setToken($tokenId, $value); + + return new CsrfToken($tokenId, $value); + } + + /** + * {@inheritdoc} + */ + public function removeToken($tokenId) + { + return $this->storage->removeToken($tokenId); + } + + /** + * {@inheritdoc} + */ + public function isTokenValid(CsrfToken $token) + { + if (!$this->storage->hasToken($token->getId())) { + return false; + } + + return StringUtils::equals((string) $this->storage->getToken($token->getId()), $token->getValue()); + } +} diff --git a/Csrf/CsrfTokenManagerInterface.php b/Csrf/CsrfTokenManagerInterface.php new file mode 100644 index 0000000..878237b --- /dev/null +++ b/Csrf/CsrfTokenManagerInterface.php @@ -0,0 +1,67 @@ +<?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; + +/** + * Manages CSRF tokens. + * + * @since 2.4 + * @author Bernhard Schussek <bschussek@gmail.com> + */ +interface CsrfTokenManagerInterface +{ + /** + * Returns a CSRF token for the given ID. + * + * If previously no token existed for the given ID, a new token is + * generated. Otherwise the existing token is returned. + * + * @param string $tokenId The token ID. You may choose an arbitrary value + * for the ID + * + * @return CsrfToken The CSRF token + */ + public function getToken($tokenId); + + /** + * Generates a new token value for the given ID. + * + * This method will generate a new token for the given token ID, independent + * of whether a token value previously existed or not. It can be used to + * enforce once-only tokens in environments with high security needs. + * + * @param string $tokenId The token ID. You may choose an arbitrary value + * for the ID + * + * @return CsrfToken The CSRF token + */ + public function refreshToken($tokenId); + + /** + * Invalidates the CSRF token with the given ID, if one exists. + * + * @param string $tokenId The token ID + * + * @return Boolean Returns true if a token existed for this ID, false + * otherwise + */ + public function removeToken($tokenId); + + /** + * Returns whether the given CSRF token is valid. + * + * @param CsrfToken $token A CSRF token + * + * @return Boolean Returns true if the token is valid, false otherwise + */ + public function isTokenValid(CsrfToken $token); +} diff --git a/Csrf/Exception/TokenNotFoundException.php b/Csrf/Exception/TokenNotFoundException.php new file mode 100644 index 0000000..936afde --- /dev/null +++ b/Csrf/Exception/TokenNotFoundException.php @@ -0,0 +1,21 @@ +<?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\Exception; + +use Symfony\Component\Security\Core\Exception\RuntimeException; + +/** + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class TokenNotFoundException extends RuntimeException +{ +} diff --git a/Csrf/README.md b/Csrf/README.md index 394a29e..2b51362 100644 --- a/Csrf/README.md +++ b/Csrf/README.md @@ -2,7 +2,7 @@ Security Component - CSRF ========================= The Security CSRF (cross-site request forgery) component provides a class -`CsrfTokenGenerator` for generating and validating CSRF tokens. +`CsrfTokenManager` for generating and validating CSRF tokens. Resources --------- diff --git a/Csrf/Tests/CsrfTokenGeneratorTest.php b/Csrf/Tests/CsrfTokenGeneratorTest.php deleted file mode 100644 index f5f9507..0000000 --- a/Csrf/Tests/CsrfTokenGeneratorTest.php +++ /dev/null @@ -1,148 +0,0 @@ -<?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/CsrfTokenManagerTest.php b/Csrf/Tests/CsrfTokenManagerTest.php new file mode 100644 index 0000000..67c66fb --- /dev/null +++ b/Csrf/Tests/CsrfTokenManagerTest.php @@ -0,0 +1,164 @@ +<?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\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManager; + +/** + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class CsrfTokenManagerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $generator; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $storage; + + /** + * @var CsrfTokenManager + */ + private $manager; + + protected function setUp() + { + $this->generator = $this->getMock('Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface'); + $this->storage = $this->getMock('Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface'); + $this->manager = new CsrfTokenManager($this->generator, $this->storage); + } + + protected function tearDown() + { + $this->generator = null; + $this->storage = null; + $this->manager = null; + } + + public function testGetNonExistingToken() + { + $this->storage->expects($this->once()) + ->method('hasToken') + ->with('token_id') + ->will($this->returnValue(false)); + + $this->generator->expects($this->once()) + ->method('generateToken') + ->will($this->returnValue('TOKEN')); + + $this->storage->expects($this->once()) + ->method('setToken') + ->with('token_id', 'TOKEN'); + + $token = $this->manager->getToken('token_id'); + + $this->assertInstanceOf('Symfony\Component\Security\Csrf\CsrfToken', $token); + $this->assertSame('token_id', $token->getId()); + $this->assertSame('TOKEN', $token->getValue()); + } + + public function testUseExistingTokenIfAvailable() + { + $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')); + + $token = $this->manager->getToken('token_id'); + + $this->assertInstanceOf('Symfony\Component\Security\Csrf\CsrfToken', $token); + $this->assertSame('token_id', $token->getId()); + $this->assertSame('TOKEN', $token->getValue()); + } + + public function testRefreshTokenAlwaysReturnsNewToken() + { + $this->storage->expects($this->never()) + ->method('hasToken'); + + $this->generator->expects($this->once()) + ->method('generateToken') + ->will($this->returnValue('TOKEN')); + + $this->storage->expects($this->once()) + ->method('setToken') + ->with('token_id', 'TOKEN'); + + $token = $this->manager->refreshToken('token_id'); + + $this->assertInstanceOf('Symfony\Component\Security\Csrf\CsrfToken', $token); + $this->assertSame('token_id', $token->getId()); + $this->assertSame('TOKEN', $token->getValue()); + } + + 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->manager->isTokenValid(new CsrfToken('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->manager->isTokenValid(new CsrfToken('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->manager->isTokenValid(new CsrfToken('token_id', 'FOOBAR'))); + } + + public function testRemoveToken() + { + $this->storage->expects($this->once()) + ->method('removeToken') + ->with('token_id') + ->will($this->returnValue('REMOVED_TOKEN')); + + $this->assertSame('REMOVED_TOKEN', $this->manager->removeToken('token_id')); + } +} diff --git a/Csrf/Tests/TokenGenerator/UriSafeTokenGeneratorTest.php b/Csrf/Tests/TokenGenerator/UriSafeTokenGeneratorTest.php new file mode 100644 index 0000000..a55056f --- /dev/null +++ b/Csrf/Tests/TokenGenerator/UriSafeTokenGeneratorTest.php @@ -0,0 +1,71 @@ +<?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\TokenGenerator; + +use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator; + +/** + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class UriSafeTokenGeneratorTest extends \PHPUnit_Framework_TestCase +{ + const ENTROPY = 1000; + + /** + * A non alpha-numeric byte string + * @var string + */ + private static $bytes; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $random; + + + /** + * @var UriSafeTokenGenerator + */ + 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->generator = new UriSafeTokenGenerator($this->random, self::ENTROPY); + } + + protected function tearDown() + { + $this->random = null; + $this->generator = null; + } + + public function testGenerateToken() + { + $this->random->expects($this->once()) + ->method('nextBytes') + ->with(self::ENTROPY/8) + ->will($this->returnValue(self::$bytes)); + + $token = $this->generator->generateToken(); + + $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'); + } +} diff --git a/Csrf/Tests/TokenStorage/NativeSessionTokenStorageTest.php b/Csrf/Tests/TokenStorage/NativeSessionTokenStorageTest.php index b62eeef..ada04c8 100644 --- a/Csrf/Tests/TokenStorage/NativeSessionTokenStorageTest.php +++ b/Csrf/Tests/TokenStorage/NativeSessionTokenStorageTest.php @@ -96,8 +96,31 @@ class NativeSessionTokenStorageTest extends \PHPUnit_Framework_TestCase $this->assertSame('TOKEN', $this->storage->getToken('token_id')); } + /** + * @expectedException \Symfony\Component\Security\Csrf\Exception\TokenNotFoundException + */ public function testGetNonExistingToken() { - $this->assertSame('DEFAULT', $this->storage->getToken('token_id', 'DEFAULT')); + $this->storage->getToken('token_id'); + } + + /** + * @depends testCheckToken + */ + public function testRemoveNonExistingToken() + { + $this->assertNull($this->storage->removeToken('token_id')); + $this->assertFalse($this->storage->hasToken('token_id')); + } + + /** + * @depends testCheckToken + */ + public function testRemoveExistingToken() + { + $this->storage->setToken('token_id', 'TOKEN'); + + $this->assertSame('TOKEN', $this->storage->removeToken('token_id')); + $this->assertFalse($this->storage->hasToken('token_id')); } } diff --git a/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php b/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php index 9ba7edb..799b16d 100644 --- a/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php +++ b/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php @@ -108,7 +108,7 @@ class SessionTokenStorageTest extends \PHPUnit_Framework_TestCase $this->assertSame('RESULT', $this->storage->hasToken('token_id')); } - public function testGetTokenFromClosedSession() + public function testGetExistingTokenFromClosedSession() { $this->session->expects($this->any()) ->method('isStarted') @@ -118,14 +118,19 @@ class SessionTokenStorageTest extends \PHPUnit_Framework_TestCase ->method('start'); $this->session->expects($this->once()) + ->method('has') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue(true)); + + $this->session->expects($this->once()) ->method('get') - ->with(self::SESSION_NAMESPACE.'/token_id', 'DEFAULT') + ->with(self::SESSION_NAMESPACE.'/token_id') ->will($this->returnValue('RESULT')); - $this->assertSame('RESULT', $this->storage->getToken('token_id', 'DEFAULT')); + $this->assertSame('RESULT', $this->storage->getToken('token_id')); } - public function testGetTokenFromActiveSession() + public function testGetExistingTokenFromActiveSession() { $this->session->expects($this->any()) ->method('isStarted') @@ -135,10 +140,123 @@ class SessionTokenStorageTest extends \PHPUnit_Framework_TestCase ->method('start'); $this->session->expects($this->once()) + ->method('has') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue(true)); + + $this->session->expects($this->once()) ->method('get') - ->with(self::SESSION_NAMESPACE.'/token_id', 'DEFAULT') + ->with(self::SESSION_NAMESPACE.'/token_id') ->will($this->returnValue('RESULT')); - $this->assertSame('RESULT', $this->storage->getToken('token_id', 'DEFAULT')); + $this->assertSame('RESULT', $this->storage->getToken('token_id')); + } + + /** + * @expectedException \Symfony\Component\Security\Csrf\Exception\TokenNotFoundException + */ + public function testGetNonExistingTokenFromClosedSession() + { + $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(false)); + + $this->storage->getToken('token_id'); + } + + /** + * @expectedException \Symfony\Component\Security\Csrf\Exception\TokenNotFoundException + */ + public function testGetNonExistingTokenFromActiveSession() + { + $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(false)); + + $this->storage->getToken('token_id'); + } + + public function testRemoveNonExistingTokenFromClosedSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(false)); + + $this->session->expects($this->once()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('remove') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue(null)); + + $this->assertNull($this->storage->removeToken('token_id')); + } + + public function testRemoveNonExistingTokenFromActiveSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(true)); + + $this->session->expects($this->never()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('remove') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue(null)); + + $this->assertNull($this->storage->removeToken('token_id')); + } + + public function testRemoveExistingTokenFromClosedSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(false)); + + $this->session->expects($this->once()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('remove') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue('TOKEN')); + + $this->assertSame('TOKEN', $this->storage->removeToken('token_id')); + } + + public function testRemoveExistingTokenFromActiveSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(true)); + + $this->session->expects($this->never()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('remove') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue('TOKEN')); + + $this->assertSame('TOKEN', $this->storage->removeToken('token_id')); } } diff --git a/Csrf/CsrfTokenGeneratorInterface.php b/Csrf/TokenGenerator/TokenGeneratorInterface.php index c34549f..4d81da9 100644 --- a/Csrf/CsrfTokenGeneratorInterface.php +++ b/Csrf/TokenGenerator/TokenGeneratorInterface.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Csrf; +namespace Symfony\Component\Security\Csrf\TokenGenerator; /** * Generates and validates CSRF tokens. @@ -29,24 +29,12 @@ namespace Symfony\Component\Security\Csrf; * @since 2.4 * @author Bernhard Schussek <bschussek@gmail.com> */ -interface CsrfTokenGeneratorInterface +interface TokenGeneratorInterface { /** - * Generates a CSRF token with the given token ID. - * - * @param string $tokenId An ID that identifies the token + * Generates a CSRF 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); + public function generateToken(); } diff --git a/Csrf/TokenGenerator/UriSafeTokenGenerator.php b/Csrf/TokenGenerator/UriSafeTokenGenerator.php new file mode 100644 index 0000000..9d24f1c --- /dev/null +++ b/Csrf/TokenGenerator/UriSafeTokenGenerator.php @@ -0,0 +1,73 @@ +<?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\TokenGenerator; + +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 CSRF tokens. + * + * @since 2.4 + * @author Bernhard Schussek <bernhard.schussek@symfony.com> + */ +class UriSafeTokenGenerator implements TokenGeneratorInterface +{ + /** + * The generator for random values. + * + * @var SecureRandomInterface + */ + private $random; + + /** + * The amount of entropy collected for each token (in bits). + * + * @var integer + */ + private $entropy; + + /** + * Generates URI-safe CSRF tokens. + * + * @param SecureRandomInterface $random The random value generator used for + * generating entropy + * @param integer $entropy The amount of entropy collected for + * each token (in bits) + * + */ + public function __construct(SecureRandomInterface $random = null, $entropy = 256) + { + if (null === $random) { + $random = new SecureRandom(); + } + + $this->random = $random; + $this->entropy = $entropy; + } + + /** + * {@inheritDoc} + */ + public function generateToken() + { + // 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); + + return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '='); + } +} diff --git a/Csrf/TokenStorage/NativeSessionTokenStorage.php b/Csrf/TokenStorage/NativeSessionTokenStorage.php index 8956743..c01967c 100644 --- a/Csrf/TokenStorage/NativeSessionTokenStorage.php +++ b/Csrf/TokenStorage/NativeSessionTokenStorage.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Security\Csrf\TokenStorage; +use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException; + /** * Token storage that uses PHP's native session handling. * @@ -49,17 +51,17 @@ class NativeSessionTokenStorage implements TokenStorageInterface /** * {@inheritdoc} */ - public function getToken($tokenId, $default = null) + public function getToken($tokenId) { if (!$this->sessionStarted) { $this->startSession(); } - if (isset($_SESSION[$this->namespace][$tokenId])) { - return $_SESSION[$this->namespace][$tokenId]; + if (!isset($_SESSION[$this->namespace][$tokenId])) { + throw new TokenNotFoundException('The CSRF token with ID '.$tokenId.' does not exist.'); } - return $default; + return (string) $_SESSION[$this->namespace][$tokenId]; } /** @@ -71,7 +73,7 @@ class NativeSessionTokenStorage implements TokenStorageInterface $this->startSession(); } - $_SESSION[$this->namespace][$tokenId] = $token; + $_SESSION[$this->namespace][$tokenId] = (string) $token; } /** @@ -86,6 +88,24 @@ class NativeSessionTokenStorage implements TokenStorageInterface return isset($_SESSION[$this->namespace][$tokenId]); } + /** + * {@inheritdoc} + */ + public function removeToken($tokenId) + { + if (!$this->sessionStarted) { + $this->startSession(); + } + + $token = isset($_SESSION[$this->namespace][$tokenId]) + ? $_SESSION[$this->namespace][$tokenId] + : null; + + unset($_SESSION[$this->namespace][$tokenId]); + + return $token; + } + private function startSession() { if (version_compare(PHP_VERSION, '5.4', '>=')) { diff --git a/Csrf/TokenStorage/SessionTokenStorage.php b/Csrf/TokenStorage/SessionTokenStorage.php index 3878e4c..f08eb96 100644 --- a/Csrf/TokenStorage/SessionTokenStorage.php +++ b/Csrf/TokenStorage/SessionTokenStorage.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Csrf\TokenStorage; use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException; /** * Token storage that uses a Symfony2 Session object. @@ -54,13 +55,17 @@ class SessionTokenStorage implements TokenStorageInterface /** * {@inheritdoc} */ - public function getToken($tokenId, $default = null) + public function getToken($tokenId) { if (!$this->session->isStarted()) { $this->session->start(); } - return $this->session->get($this->namespace . '/' . $tokenId, $default); + if (!$this->session->has($this->namespace.'/'.$tokenId)) { + throw new TokenNotFoundException('The CSRF token with ID '.$tokenId.' does not exist.'); + } + + return (string) $this->session->get($this->namespace.'/'.$tokenId); } /** @@ -72,7 +77,7 @@ class SessionTokenStorage implements TokenStorageInterface $this->session->start(); } - $this->session->set($this->namespace . '/' . $tokenId, $token); + $this->session->set($this->namespace.'/'.$tokenId, (string) $token); } /** @@ -84,6 +89,18 @@ class SessionTokenStorage implements TokenStorageInterface $this->session->start(); } - return $this->session->has($this->namespace . '/' . $tokenId); + return $this->session->has($this->namespace.'/'.$tokenId); + } + + /** + * {@inheritdoc} + */ + public function removeToken($tokenId) + { + if (!$this->session->isStarted()) { + $this->session->start(); + } + + return $this->session->remove($this->namespace.'/'.$tokenId); } } diff --git a/Csrf/TokenStorage/TokenStorageInterface.php b/Csrf/TokenStorage/TokenStorageInterface.php index 7dba9e5..3fb3191 100644 --- a/Csrf/TokenStorage/TokenStorageInterface.php +++ b/Csrf/TokenStorage/TokenStorageInterface.php @@ -23,27 +23,37 @@ 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 + * @return string The stored token + * + * @throws \Symfony\Component\Security\Csrf\Exception\TokenNotFoundException If the token ID does not exist */ - public function getToken($tokenId, $default = null); + public function getToken($tokenId); /** * Stores a CSRF token. * * @param string $tokenId The token ID - * @param mixed $token The CSRF token + * @param string $token The CSRF token */ public function setToken($tokenId, $token); /** + * Removes a CSRF token. + * + * @param string $tokenId The token ID + * + * @return string|null Returns the removed token if one existed, NULL + * otherwise + */ + public function removeToken($tokenId); + + /** * 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. + * @return Boolean Whether a token exists with the given ID */ public function hasToken($tokenId); } |