diff --git a/CHANGELOG.md b/CHANGELOG.md index 1476c99..a347990 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.2 +--- + + * Add `SameOriginCsrfTokenManager` + 6.0 --- diff --git a/CsrfToken.php b/CsrfToken.php index 57f972e..f3a28d9 100644 --- a/CsrfToken.php +++ b/CsrfToken.php @@ -18,12 +18,12 @@ */ class CsrfToken { - private string $id; private string $value; - public function __construct(string $id, #[\SensitiveParameter] ?string $value) - { - $this->id = $id; + public function __construct( + private string $id, + #[\SensitiveParameter] ?string $value, + ) { $this->value = $value ?? ''; } diff --git a/CsrfTokenManager.php b/CsrfTokenManager.php index 96ecc54..1a873ae 100644 --- a/CsrfTokenManager.php +++ b/CsrfTokenManager.php @@ -47,7 +47,7 @@ public function __construct(?TokenGeneratorInterface $generator = null, ?TokenSt if (null === $namespace) { $this->namespace = $superGlobalNamespaceGenerator; } elseif ($namespace instanceof RequestStack) { - $this->namespace = function () use ($namespace, $superGlobalNamespaceGenerator) { + $this->namespace = static function () use ($namespace, $superGlobalNamespaceGenerator) { if ($request = $namespace->getMainRequest()) { return $request->isSecure() ? 'https-' : ''; } @@ -59,7 +59,7 @@ public function __construct(?TokenGeneratorInterface $generator = null, ?TokenSt } elseif (\is_callable($namespace)) { $this->namespace = $namespace(...); } else { - throw new InvalidArgumentException(sprintf('$namespace must be a string, a callable returning a string, null or an instance of "RequestStack". "%s" given.', get_debug_type($namespace))); + throw new InvalidArgumentException(\sprintf('$namespace must be a string, a callable returning a string, null or an instance of "RequestStack". "%s" given.', get_debug_type($namespace))); } } @@ -112,7 +112,7 @@ private function randomize(string $value): string $key = random_bytes(32); $value = $this->xor($value, $key); - return sprintf('%s.%s.%s', substr(hash('xxh128', $key), 0, 1 + (\ord($key[0]) % 32)), rtrim(strtr(base64_encode($key), '+/', '-_'), '='), rtrim(strtr(base64_encode($value), '+/', '-_'), '=')); + return \sprintf('%s.%s.%s', substr(hash('xxh128', $key), 0, 1 + (\ord($key[0]) % 32)), rtrim(strtr(base64_encode($key), '+/', '-_'), '='), rtrim(strtr(base64_encode($value), '+/', '-_'), '=')); } private function derandomize(string $value): string diff --git a/README.md b/README.md index 90b7bfe..79277e6 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ The Security CSRF (cross-site request forgery) component provides a class Sponsor ------- -The Security component for Symfony 6.4 is [backed][1] by [SymfonyCasts][2]. +The Security component for Symfony 7.1 is [backed][1] by [SymfonyCasts][2]. Learn Symfony faster by watching real projects being built and actively coding along with them. SymfonyCasts bridges that learning gap, bringing you video diff --git a/SameOriginCsrfTokenManager.php b/SameOriginCsrfTokenManager.php new file mode 100644 index 0000000..b1e0730 --- /dev/null +++ b/SameOriginCsrfTokenManager.php @@ -0,0 +1,288 @@ + + * + * 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 Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpKernel\Event\ResponseEvent; + +/** + * This CSRF token manager uses a combination of cookie and headers to validate non-persistent tokens. + * + * This manager is designed to be stateless and compatible with HTTP-caching. + * + * First, we validate the source of the request using the Origin/Referer headers. This relies + * on the app being able to know its own target origin. Don't miss configuring your reverse proxy to + * send the X-Forwarded-* / Forwarded headers if you're behind one. + * + * Then, we validate the request using a cookie and a CsrfToken. If the cookie is found, it should + * contain the same value as the CsrfToken. A JavaScript snippet on the client side is responsible + * for performing this double-submission. The token value should be regenerated on every request + * using a cryptographically secure random generator. + * + * If either double-submit or Origin/Referer headers are missing, it typically indicates that + * JavaScript is disabled on the client side, or that the JavaScript snippet was not properly + * implemented, or that the Origin/Referer headers were filtered out. + * + * Requests lacking both double-submit and origin information are deemed insecure. + * + * When a session is found, a behavioral check is added to ensure that the validation method does not + * downgrade from double-submit to origin checks. This prevents attackers from exploiting potentially + * less secure validation methods once a more secure method has been confirmed as functional. + * + * On HTTPS connections, the cookie is prefixed with "__Host-" to prevent it from being forged on an + * HTTP channel. On the JS side, the cookie should be set with samesite=strict to strengthen the CSRF + * protection. The cookie is always cleared on the response to prevent any further use of the token. + * + * The $checkHeader argument allows the token to be checked in a header instead of or in addition to a + * cookie. This makes it harder for an attacker to forge a request, though it may also pose challenges + * when setting the header depending on the client-side framework in use. + * + * When a fallback CSRF token manager is provided, only tokens listed in the $tokenIds argument will be + * managed by this manager. All other tokens will be delegated to the fallback manager. + * + * @author Nicolas Grekas + */ +final class SameOriginCsrfTokenManager implements CsrfTokenManagerInterface +{ + public const TOKEN_MIN_LENGTH = 24; + + public const CHECK_NO_HEADER = 0; + public const CHECK_HEADER = 1; + public const CHECK_ONLY_HEADER = 2; + + /** + * @param self::CHECK_* $checkHeader + * @param string[] $tokenIds + */ + public function __construct( + private RequestStack $requestStack, + private ?LoggerInterface $logger = null, + private ?CsrfTokenManagerInterface $fallbackCsrfTokenManager = null, + private array $tokenIds = [], + private int $checkHeader = self::CHECK_NO_HEADER, + private string $cookieName = 'csrf-token', + ) { + if (!$cookieName) { + throw new \InvalidArgumentException('The cookie name cannot be empty.'); + } + + if (!preg_match('/^[-a-zA-Z0-9_]+$/D', $cookieName)) { + throw new \InvalidArgumentException('The cookie name contains invalid characters.'); + } + + $this->tokenIds = array_flip($tokenIds); + } + + public function getToken(string $tokenId): CsrfToken + { + if (!isset($this->tokenIds[$tokenId]) && $this->fallbackCsrfTokenManager) { + return $this->fallbackCsrfTokenManager->getToken($tokenId); + } + + return new CsrfToken($tokenId, $this->cookieName); + } + + public function refreshToken(string $tokenId): CsrfToken + { + if (!isset($this->tokenIds[$tokenId]) && $this->fallbackCsrfTokenManager) { + return $this->fallbackCsrfTokenManager->refreshToken($tokenId); + } + + return new CsrfToken($tokenId, $this->cookieName); + } + + public function removeToken(string $tokenId): ?string + { + if (!isset($this->tokenIds[$tokenId]) && $this->fallbackCsrfTokenManager) { + return $this->fallbackCsrfTokenManager->removeToken($tokenId); + } + + return null; + } + + public function isTokenValid(CsrfToken $token): bool + { + if (!isset($this->tokenIds[$token->getId()]) && $this->fallbackCsrfTokenManager) { + return $this->fallbackCsrfTokenManager->isTokenValid($token); + } + + if (!$request = $this->requestStack->getCurrentRequest()) { + $this->logger?->error('CSRF validation failed: No request found.'); + + return false; + } + + if (\strlen($token->getValue()) < self::TOKEN_MIN_LENGTH && $token->getValue() !== $this->cookieName) { + $this->logger?->warning('Invalid double-submit CSRF token.'); + + return false; + } + + if (false === $isValidOrigin = $this->isValidOrigin($request)) { + $this->logger?->warning('CSRF validation failed: origin info doesn\'t match.'); + + return false; + } + + if (false === $isValidDoubleSubmit = $this->isValidDoubleSubmit($request, $token->getValue())) { + return false; + } + + if (null === $isValidOrigin && null === $isValidDoubleSubmit) { + $this->logger?->warning('CSRF validation failed: double-submit and origin info not found.'); + + return false; + } + + // Opportunistically lookup at the session for a previous CSRF validation strategy + $session = $request->hasPreviousSession() ? $request->getSession() : null; + $usageIndexValue = $session instanceof Session ? $usageIndexReference = &$session->getUsageIndex() : 0; + $usageIndexReference = \PHP_INT_MIN; + $previousCsrfProtection = (int) $session?->get($this->cookieName); + $usageIndexReference = $usageIndexValue; + $shift = $request->isMethodSafe() ? 8 : 0; + + if ($previousCsrfProtection) { + if (!$isValidOrigin && (1 & ($previousCsrfProtection >> $shift))) { + $this->logger?->warning('CSRF validation failed: origin info was used in a previous request but is now missing.'); + + return false; + } + + if (!$isValidDoubleSubmit && (2 & ($previousCsrfProtection >> $shift))) { + $this->logger?->warning('CSRF validation failed: double-submit info was used in a previous request but is now missing.'); + + return false; + } + } + + if ($isValidOrigin && $isValidDoubleSubmit) { + $csrfProtection = 3; + $this->logger?->debug('CSRF validation accepted using both origin and double-submit info.'); + } elseif ($isValidOrigin) { + $csrfProtection = 1; + $this->logger?->debug('CSRF validation accepted using origin info.'); + } else { + $csrfProtection = 2; + $this->logger?->debug('CSRF validation accepted using double-submit info.'); + } + + if (1 & $csrfProtection) { + // Persist valid origin for both safe and non-safe requests + $previousCsrfProtection |= 1 & (1 << 8); + } + + $request->attributes->set($this->cookieName, ($csrfProtection << $shift) | $previousCsrfProtection); + + return true; + } + + public function clearCookies(Request $request, Response $response): void + { + if (!$request->attributes->has($this->cookieName)) { + return; + } + + $cookieName = ($request->isSecure() ? '__Host-' : '').$this->cookieName; + + foreach ($request->cookies->all() as $name => $value) { + if ($this->cookieName === $value && str_starts_with($name, $cookieName.'_')) { + $response->headers->clearCookie($name, '/', null, $request->isSecure(), false, 'strict'); + } + } + } + + public function persistStrategy(Request $request): void + { + if (!$request->attributes->has($this->cookieName) + || !$request->hasSession(true) + || !($session = $request->getSession())->isStarted() + ) { + return; + } + + $usageIndexValue = $session instanceof Session ? $usageIndexReference = &$session->getUsageIndex() : 0; + $usageIndexReference = \PHP_INT_MIN; + $session->set($this->cookieName, $request->attributes->get($this->cookieName)); + $usageIndexReference = $usageIndexValue; + } + + public function onKernelResponse(ResponseEvent $event): void + { + if (!$event->isMainRequest()) { + return; + } + + $this->clearCookies($event->getRequest(), $event->getResponse()); + $this->persistStrategy($event->getRequest()); + } + + /** + * @return bool|null Whether the origin is valid, null if missing + */ + private function isValidOrigin(Request $request): ?bool + { + $target = $request->getSchemeAndHttpHost().'/'; + $source = 'null'; + + foreach (['Origin', 'Referer'] as $header) { + if (!$request->headers->has($header)) { + continue; + } + $source = $request->headers->get($header); + + if (str_starts_with($source.'/', $target)) { + return true; + } + } + + return 'null' === $source ? null : false; + } + + /** + * @return bool|null Whether the double-submit is valid, null if missing + */ + private function isValidDoubleSubmit(Request $request, string $token): ?bool + { + if ($this->cookieName === $token) { + return null; + } + + if ($this->checkHeader && $request->headers->get($this->cookieName, $token) !== $token) { + $this->logger?->warning('CSRF validation failed: wrong token found in header info.'); + + return false; + } + + $cookieName = ($request->isSecure() ? '__Host-' : '').$this->cookieName; + + if (self::CHECK_ONLY_HEADER === $this->checkHeader) { + if (!$request->headers->has($this->cookieName)) { + return null; + } + + $request->cookies->set($cookieName.'_'.$token, $this->cookieName); // Ensure clearCookie() can remove any cookie filtered by a reverse-proxy + + return true; + } + + if (($request->cookies->all()[$cookieName.'_'.$token] ?? null) !== $this->cookieName && !($this->checkHeader && $request->headers->has($this->cookieName))) { + return null; + } + + return true; + } +} diff --git a/Tests/CsrfTokenManagerTest.php b/Tests/CsrfTokenManagerTest.php index 44878b6..d33adfb 100644 --- a/Tests/CsrfTokenManagerTest.php +++ b/Tests/CsrfTokenManagerTest.php @@ -546,8 +546,6 @@ protected function setUp(): void protected function tearDown(): void { - parent::tearDown(); - unset($_SERVER['HTTPS']); } } diff --git a/Tests/SameOriginCsrfTokenManagerTest.php b/Tests/SameOriginCsrfTokenManagerTest.php new file mode 100644 index 0000000..0a215d2 --- /dev/null +++ b/Tests/SameOriginCsrfTokenManagerTest.php @@ -0,0 +1,261 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Csrf\Tests; + +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpKernel\Event\ResponseEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\SameOriginCsrfTokenManager; + +class SameOriginCsrfTokenManagerTest extends TestCase +{ + private $requestStack; + private $logger; + private $csrfTokenManager; + + protected function setUp(): void + { + $this->requestStack = new RequestStack(); + $this->logger = $this->createMock(LoggerInterface::class); + $this->csrfTokenManager = new SameOriginCsrfTokenManager($this->requestStack, $this->logger); + } + + public function testInvalidCookieName() + { + $this->expectException(\InvalidArgumentException::class); + new SameOriginCsrfTokenManager($this->requestStack, $this->logger, null, [], SameOriginCsrfTokenManager::CHECK_NO_HEADER, ''); + } + + public function testInvalidCookieNameCharacters() + { + $this->expectException(\InvalidArgumentException::class); + new SameOriginCsrfTokenManager($this->requestStack, $this->logger, null, [], SameOriginCsrfTokenManager::CHECK_NO_HEADER, 'invalid name!'); + } + + public function testGetToken() + { + $tokenId = 'test_token'; + $token = $this->csrfTokenManager->getToken($tokenId); + + $this->assertInstanceOf(CsrfToken::class, $token); + $this->assertSame($tokenId, $token->getId()); + } + + public function testNoRequest() + { + $token = new CsrfToken('test_token', 'test_value'); + + $this->logger->expects($this->once())->method('error')->with('CSRF validation failed: No request found.'); + $this->assertFalse($this->csrfTokenManager->isTokenValid($token)); + } + + public function testInvalidTokenLength() + { + $request = new Request(); + $this->requestStack->push($request); + + $token = new CsrfToken('test_token', ''); + + $this->logger->expects($this->once())->method('warning')->with('Invalid double-submit CSRF token.'); + $this->assertFalse($this->csrfTokenManager->isTokenValid($token)); + } + + public function testInvalidOrigin() + { + $request = new Request(); + $request->headers->set('Origin', 'http://malicious.com'); + $this->requestStack->push($request); + + $token = new CsrfToken('test_token', str_repeat('a', 24)); + + $this->logger->expects($this->once())->method('warning')->with('CSRF validation failed: origin info doesn\'t match.'); + $this->assertFalse($this->csrfTokenManager->isTokenValid($token)); + } + + public function testValidOrigin() + { + $request = new Request(); + $request->headers->set('Origin', $request->getSchemeAndHttpHost()); + $this->requestStack->push($request); + + $token = new CsrfToken('test_token', str_repeat('a', 24)); + + $this->logger->expects($this->once())->method('debug')->with('CSRF validation accepted using origin info.'); + $this->assertTrue($this->csrfTokenManager->isTokenValid($token)); + $this->assertSame(1 << 8, $request->attributes->get('csrf-token')); + } + + public function testValidRefererInvalidOrigin() + { + $request = new Request(); + $request->headers->set('Origin', 'http://localhost:1234'); + $request->headers->set('Referer', $request->getSchemeAndHttpHost()); + $this->requestStack->push($request); + + $token = new CsrfToken('test_token', str_repeat('a', 24)); + + $this->logger->expects($this->once())->method('debug')->with('CSRF validation accepted using origin info.'); + $this->assertTrue($this->csrfTokenManager->isTokenValid($token)); + $this->assertSame(1 << 8, $request->attributes->get('csrf-token')); + } + + public function testValidOriginAfterDoubleSubmit() + { + $session = $this->createMock(Session::class); + $request = new Request(); + $request->setSession($session); + $request->headers->set('Origin', $request->getSchemeAndHttpHost()); + $request->cookies->set('sess', 'id'); + $this->requestStack->push($request); + + $token = new CsrfToken('test_token', str_repeat('a', 24)); + + $session->expects($this->once())->method('getName')->willReturn('sess'); + $session->expects($this->once())->method('get')->with('csrf-token')->willReturn(2 << 8); + $this->logger->expects($this->once())->method('warning')->with('CSRF validation failed: double-submit info was used in a previous request but is now missing.'); + $this->assertFalse($this->csrfTokenManager->isTokenValid($token)); + } + + public function testMissingPreviousOrigin() + { + $session = $this->createMock(Session::class); + $request = new Request(); + $request->cookies->set('csrf-token_'.str_repeat('a', 24), 'csrf-token'); + $request->setSession($session); + $request->cookies->set('sess', 'id'); + $this->requestStack->push($request); + + $token = new CsrfToken('test_token', str_repeat('a', 24)); + + $session->expects($this->once())->method('getName')->willReturn('sess'); + $session->expects($this->once())->method('get')->with('csrf-token')->willReturn(1 << 8); + $this->logger->expects($this->once())->method('warning')->with('CSRF validation failed: origin info was used in a previous request but is now missing.'); + $this->assertFalse($this->csrfTokenManager->isTokenValid($token)); + } + + public function testValidDoubleSubmit() + { + $request = new Request(); + $request->cookies->set('csrf-token_'.str_repeat('a', 24), 'csrf-token'); + $this->requestStack->push($request); + + $token = new CsrfToken('test_token', str_repeat('a', 24)); + + $this->logger->expects($this->once())->method('debug')->with('CSRF validation accepted using double-submit info.'); + $this->assertTrue($this->csrfTokenManager->isTokenValid($token)); + $this->assertSame(2 << 8, $request->attributes->get('csrf-token')); + } + + public function testCheckOnlyHeader() + { + $csrfTokenManager = new SameOriginCsrfTokenManager($this->requestStack, $this->logger, null, [], SameOriginCsrfTokenManager::CHECK_ONLY_HEADER); + + $request = new Request(); + $tokenValue = str_repeat('a', 24); + $request->headers->set('csrf-token', $tokenValue); + $this->requestStack->push($request); + + $token = new CsrfToken('test_token', $tokenValue); + + $this->logger->expects($this->once())->method('debug')->with('CSRF validation accepted using double-submit info.'); + $this->assertTrue($csrfTokenManager->isTokenValid($token)); + $this->assertSame('csrf-token', $request->cookies->get('csrf-token_'.$tokenValue)); + + $this->logger->expects($this->once())->method('warning')->with('CSRF validation failed: wrong token found in header info.'); + $this->assertFalse($csrfTokenManager->isTokenValid(new CsrfToken('test_token', str_repeat('b', 24)))); + } + + /** + * @testWith [0] + * [1] + * [2] + */ + public function testValidOriginMissingDoubleSubmit(int $checkHeader) + { + $csrfTokenManager = new SameOriginCsrfTokenManager($this->requestStack, $this->logger, null, [], $checkHeader); + + $request = new Request(); + $tokenValue = str_repeat('a', 24); + $request->headers->set('Origin', $request->getSchemeAndHttpHost()); + $this->requestStack->push($request); + + $token = new CsrfToken('test_token', $tokenValue); + + $this->logger->expects($this->once())->method('debug')->with('CSRF validation accepted using origin info.'); + $this->assertTrue($csrfTokenManager->isTokenValid($token)); + } + + public function testMissingEverything() + { + $request = new Request(); + $this->requestStack->push($request); + + $token = new CsrfToken('test_token', str_repeat('a', 24)); + + $this->logger->expects($this->once())->method('warning')->with('CSRF validation failed: double-submit and origin info not found.'); + $this->assertFalse($this->csrfTokenManager->isTokenValid($token)); + } + + public function testClearCookies() + { + $request = new Request([], [], ['csrf-token' => 2], ['csrf-token_test' => 'csrf-token']); + $response = new Response(); + + $this->csrfTokenManager->clearCookies($request, $response); + + $this->assertTrue($response->headers->has('Set-Cookie')); + } + + public function testPersistStrategyWithStartedSession() + { + $session = $this->createMock(Session::class); + $session->method('isStarted')->willReturn(true); + + $request = new Request(); + $request->setSession($session); + $request->attributes->set('csrf-token', 2 << 8); + + $session->expects($this->once())->method('set')->with('csrf-token', 2 << 8); + + $this->csrfTokenManager->persistStrategy($request); + } + + public function testPersistStrategyWithSessionNotStarted() + { + $session = $this->createMock(Session::class); + + $request = new Request(); + $request->setSession($session); + $request->attributes->set('csrf-token', 2 << 8); + + $session->expects($this->never())->method('set'); + + $this->csrfTokenManager->persistStrategy($request); + } + + public function testOnKernelResponse() + { + $request = new Request([], [], ['csrf-token' => 2], ['csrf-token_test' => 'csrf-token']); + $response = new Response(); + $event = new ResponseEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST, $response); + + $this->csrfTokenManager->onKernelResponse($event); + + $this->assertTrue($response->headers->has('Set-Cookie')); + } +} diff --git a/Tests/TokenStorage/SessionTokenStorageTest.php b/Tests/TokenStorage/SessionTokenStorageTest.php index 64e6180..593d1a7 100644 --- a/Tests/TokenStorage/SessionTokenStorageTest.php +++ b/Tests/TokenStorage/SessionTokenStorageTest.php @@ -94,8 +94,10 @@ public function testGetNonExistingTokenFromClosedSession() public function testGetNonExistingTokenFromActiveSession() { - $this->expectException(TokenNotFoundException::class); $this->session->start(); + + $this->expectException(TokenNotFoundException::class); + $this->storage->getToken('token_id'); } diff --git a/TokenGenerator/UriSafeTokenGenerator.php b/TokenGenerator/UriSafeTokenGenerator.php index a315944..a91a4aa 100644 --- a/TokenGenerator/UriSafeTokenGenerator.php +++ b/TokenGenerator/UriSafeTokenGenerator.php @@ -18,20 +18,17 @@ */ class UriSafeTokenGenerator implements TokenGeneratorInterface { - private int $entropy; - /** * Generates URI-safe CSRF tokens. * * @param int $entropy The amount of entropy collected for each token (in bits) */ - public function __construct(int $entropy = 256) - { + public function __construct( + private int $entropy = 256, + ) { if ($entropy <= 7) { throw new \InvalidArgumentException('Entropy should be greater than 7.'); } - - $this->entropy = $entropy; } public function generateToken(): string diff --git a/TokenStorage/ClearableTokenStorageInterface.php b/TokenStorage/ClearableTokenStorageInterface.php index 185c4a7..2f6d96b 100644 --- a/TokenStorage/ClearableTokenStorageInterface.php +++ b/TokenStorage/ClearableTokenStorageInterface.php @@ -18,8 +18,6 @@ interface ClearableTokenStorageInterface extends TokenStorageInterface { /** * Removes all CSRF tokens. - * - * @return void */ - public function clear(); + public function clear(): void; } diff --git a/TokenStorage/NativeSessionTokenStorage.php b/TokenStorage/NativeSessionTokenStorage.php index 7de8b52..4ab4782 100644 --- a/TokenStorage/NativeSessionTokenStorage.php +++ b/TokenStorage/NativeSessionTokenStorage.php @@ -26,16 +26,15 @@ class NativeSessionTokenStorage implements ClearableTokenStorageInterface public const SESSION_NAMESPACE = '_csrf'; private bool $sessionStarted = false; - private string $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(string $namespace = self::SESSION_NAMESPACE) - { - $this->namespace = $namespace; + public function __construct( + private string $namespace = self::SESSION_NAMESPACE, + ) { } public function getToken(string $tokenId): string @@ -51,10 +50,7 @@ public function getToken(string $tokenId): string return (string) $_SESSION[$this->namespace][$tokenId]; } - /** - * @return void - */ - public function setToken(string $tokenId, #[\SensitiveParameter] string $token) + public function setToken(string $tokenId, #[\SensitiveParameter] string $token): void { if (!$this->sessionStarted) { $this->startSession(); @@ -93,10 +89,7 @@ public function removeToken(string $tokenId): ?string return $token; } - /** - * @return void - */ - public function clear() + public function clear(): void { unset($_SESSION[$this->namespace]); } diff --git a/TokenStorage/SessionTokenStorage.php b/TokenStorage/SessionTokenStorage.php index 4b3c3e5..d5614bf 100644 --- a/TokenStorage/SessionTokenStorage.php +++ b/TokenStorage/SessionTokenStorage.php @@ -28,18 +28,15 @@ class SessionTokenStorage implements ClearableTokenStorageInterface */ public const SESSION_NAMESPACE = '_csrf'; - private RequestStack $requestStack; - private string $namespace; - /** * Initializes the storage with a RequestStack object and a session namespace. * * @param string $namespace The namespace under which the token is stored in the requestStack */ - public function __construct(RequestStack $requestStack, string $namespace = self::SESSION_NAMESPACE) - { - $this->requestStack = $requestStack; - $this->namespace = $namespace; + public function __construct( + private RequestStack $requestStack, + private string $namespace = self::SESSION_NAMESPACE, + ) { } public function getToken(string $tokenId): string @@ -56,10 +53,7 @@ public function getToken(string $tokenId): string return (string) $session->get($this->namespace.'/'.$tokenId); } - /** - * @return void - */ - public function setToken(string $tokenId, #[\SensitiveParameter] string $token) + public function setToken(string $tokenId, #[\SensitiveParameter] string $token): void { $session = $this->getSession(); if (!$session->isStarted()) { @@ -89,10 +83,7 @@ public function removeToken(string $tokenId): ?string return $session->remove($this->namespace.'/'.$tokenId); } - /** - * @return void - */ - public function clear() + public function clear(): void { $session = $this->getSession(); foreach (array_keys($session->all()) as $key) { diff --git a/TokenStorage/TokenStorageInterface.php b/TokenStorage/TokenStorageInterface.php index 32c7192..804b6a4 100644 --- a/TokenStorage/TokenStorageInterface.php +++ b/TokenStorage/TokenStorageInterface.php @@ -27,10 +27,8 @@ public function getToken(string $tokenId): string; /** * Stores a CSRF token. - * - * @return void */ - public function setToken(string $tokenId, #[\SensitiveParameter] string $token); + public function setToken(string $tokenId, #[\SensitiveParameter] string $token): void; /** * Removes a CSRF token. diff --git a/composer.json b/composer.json index 30bba30..c2bfed1 100644 --- a/composer.json +++ b/composer.json @@ -16,14 +16,16 @@ } ], "require": { - "php": ">=8.1", - "symfony/security-core": "^5.4|^6.0|^7.0" + "php": ">=8.2", + "symfony/security-core": "^6.4|^7.0" }, "require-dev": { - "symfony/http-foundation": "^5.4|^6.0|^7.0" + "psr/log": "^1|^2|^3", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" }, "conflict": { - "symfony/http-foundation": "<5.4" + "symfony/http-foundation": "<6.4" }, "autoload": { "psr-4": { "Symfony\\Component\\Security\\Csrf\\": "" },