diff --git a/UPGRADE-7.4.md b/UPGRADE-7.4.md index 80d55d57f639d..afd619fbf4253 100644 --- a/UPGRADE-7.4.md +++ b/UPGRADE-7.4.md @@ -88,6 +88,7 @@ Security * Deprecate callable firewall listeners, extend `AbstractListener` or implement `FirewallListenerInterface` instead * Deprecate `AbstractListener::__invoke` * Deprecate `LazyFirewallContext::__invoke()` + * Deprecate `PersistentTokenInterface::getClass()` and `RememberMeDetails::getUserFqcn()`, the user FQCN will be removed from the remember-me cookie in 8.0 Serializer ---------- diff --git a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php index dd1b4b2e765b3..61f1b3d998a59 100644 --- a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php +++ b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php @@ -38,9 +38,11 @@ * `series` char(88) UNIQUE PRIMARY KEY NOT NULL, * `value` char(88) NOT NULL, * `lastUsed` datetime NOT NULL, - * `class` varchar(100) NOT NULL, + * `class` varchar(100) DEFAULT '' NOT NULL, * `username` varchar(200) NOT NULL * ); + * + * (the `class` column is for BC with tables created with before Symfony 8) */ final class DoctrineTokenProvider implements TokenProviderInterface, TokenVerifierInterface { @@ -95,7 +97,7 @@ public function createNewToken(PersistentTokenInterface $token): void { $sql = 'INSERT INTO rememberme_token (class, username, series, value, lastUsed) VALUES (:class, :username, :series, :value, :lastUsed)'; $paramValues = [ - 'class' => $token->getClass(), + 'class' => method_exists($token, 'getClass') ? $token->getClass(false) : '', 'username' => $token->getUserIdentifier(), 'series' => $token->getSeries(), 'value' => $token->getTokenValue(), @@ -164,7 +166,7 @@ public function updateExistingToken(PersistentTokenInterface $token, #[\Sensitiv try { $this->deleteTokenBySeries($tmpSeries); $lastUsed = \DateTime::createFromInterface($lastUsed); - $this->createNewToken(new PersistentToken($token->getClass(), $token->getUserIdentifier(), $tmpSeries, $token->getTokenValue(), $lastUsed)); + $this->createNewToken(new PersistentToken(method_exists($token, 'getClass') ? $token->getClass(false) : '', $token->getUserIdentifier(), $tmpSeries, $token->getTokenValue(), $lastUsed)); $this->conn->commit(); } catch (\Exception $e) { @@ -195,7 +197,7 @@ private function addTableToSchema(Schema $schema): void $table->addColumn('series', Types::STRING, ['length' => 88]); $table->addColumn('value', Types::STRING, ['length' => 88]); $table->addColumn('lastUsed', Types::DATETIME_IMMUTABLE); - $table->addColumn('class', Types::STRING, ['length' => 100]); + $table->addColumn('class', Types::STRING, ['length' => 100, 'default' => '']); $table->addColumn('username', Types::STRING, ['length' => 200]); if (class_exists(PrimaryKeyConstraint::class)) { diff --git a/src/Symfony/Component/Security/Core/Authentication/RememberMe/InMemoryTokenProvider.php b/src/Symfony/Component/Security/Core/Authentication/RememberMe/InMemoryTokenProvider.php index d7fbb9c7523e9..d61a49da3cd0e 100644 --- a/src/Symfony/Component/Security/Core/Authentication/RememberMe/InMemoryTokenProvider.php +++ b/src/Symfony/Component/Security/Core/Authentication/RememberMe/InMemoryTokenProvider.php @@ -38,7 +38,7 @@ public function updateToken(string $series, #[\SensitiveParameter] string $token } $token = new PersistentToken( - $this->tokens[$series]->getClass(), + $this->tokens[$series]->getClass(false), $this->tokens[$series]->getUserIdentifier(), $series, $tokenValue, diff --git a/src/Symfony/Component/Security/Core/Authentication/RememberMe/PersistentToken.php b/src/Symfony/Component/Security/Core/Authentication/RememberMe/PersistentToken.php index 0f391c237eb1b..2d0d7e717f3de 100644 --- a/src/Symfony/Component/Security/Core/Authentication/RememberMe/PersistentToken.php +++ b/src/Symfony/Component/Security/Core/Authentication/RememberMe/PersistentToken.php @@ -43,8 +43,15 @@ public function __construct( $this->lastUsed = \DateTimeImmutable::createFromInterface($lastUsed); } - public function getClass(): string + /** + * @deprecated since Symfony 7.4 + */ + public function getClass(bool $triggerDeprecation = true): string { + if ($triggerDeprecation) { + trigger_deprecation('symfony/security-core', '7.4', 'The "%s()" method is deprecated: the user class will be removed from the remember-me cookie in 8.0.', __METHOD__); + } + return $this->class; } diff --git a/src/Symfony/Component/Security/Core/Authentication/RememberMe/PersistentTokenInterface.php b/src/Symfony/Component/Security/Core/Authentication/RememberMe/PersistentTokenInterface.php index f5c061752229f..8e036089d429a 100644 --- a/src/Symfony/Component/Security/Core/Authentication/RememberMe/PersistentTokenInterface.php +++ b/src/Symfony/Component/Security/Core/Authentication/RememberMe/PersistentTokenInterface.php @@ -21,6 +21,8 @@ interface PersistentTokenInterface { /** * Returns the class of the user. + * + * @deprecated since Symfony 7.4, the user class will be removed from the remember-me cookie in 8.0 */ public function getClass(): string; diff --git a/src/Symfony/Component/Security/Core/CHANGELOG.md b/src/Symfony/Component/Security/Core/CHANGELOG.md index e107810342433..7d2f7d027e31c 100644 --- a/src/Symfony/Component/Security/Core/CHANGELOG.md +++ b/src/Symfony/Component/Security/Core/CHANGELOG.md @@ -4,7 +4,8 @@ CHANGELOG 7.4 --- -* Add `MermaidDumper` to dump Role Hierarchy graphs in the Mermaid.js flowchart format + * Add `MermaidDumper` to dump Role Hierarchy graphs in the Mermaid.js flowchart format + * Deprecate `PersistentTokenInterface::getClass()`, the user class will be removed from the remember-me cookie in 8.0 7.3 --- diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/PersistentTokenTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/PersistentTokenTest.php index b970187959390..cf38a5214215a 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/PersistentTokenTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/PersistentTokenTest.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\RememberMe; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken; @@ -21,7 +23,6 @@ public function testConstructor() $lastUsed = new \DateTimeImmutable(); $token = new PersistentToken('fooclass', 'fooname', 'fooseries', 'footokenvalue', $lastUsed); - $this->assertEquals('fooclass', $token->getClass()); $this->assertEquals('fooname', $token->getUserIdentifier()); $this->assertEquals('fooseries', $token->getSeries()); $this->assertEquals('footokenvalue', $token->getTokenValue()); @@ -35,4 +36,12 @@ public function testDateTime() $this->assertEquals($lastUsed, $token->getLastUsed()); } + + #[IgnoreDeprecations] + #[Group('legacy')] + public function testClassDeprecation() + { + $token = new PersistentToken('fooclass', 'fooname', 'fooseries', 'footokenvalue', new \DateTimeImmutable()); + $this->assertSame('fooclass', $token->getClass()); + } } diff --git a/src/Symfony/Component/Security/Http/CHANGELOG.md b/src/Symfony/Component/Security/Http/CHANGELOG.md index c7470bed104c4..159fcf1d16dc5 100644 --- a/src/Symfony/Component/Security/Http/CHANGELOG.md +++ b/src/Symfony/Component/Security/Http/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Deprecate `AbstractListener::__invoke` * Add `$methods` argument to `#[IsGranted]` to restrict validation to specific HTTP methods * Allow subclassing `#[IsGranted]` + * Deprecate `RememberMeDetails::getUserFqcn()`, the user FQCN will be removed from the remember-me cookie in 8.0 7.3 --- diff --git a/src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php b/src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php index 37ee6803eb55b..33df939ac802e 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php +++ b/src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php @@ -65,9 +65,9 @@ public function consumeRememberMeCookie(RememberMeDetails $rememberMeDetails): U } [$series, $tokenValue] = explode(':', $rememberMeDetails->getValue(), 2); - $persistentToken = $this->tokenProvider->loadTokenBySeries($series); + $token = $this->tokenProvider->loadTokenBySeries($series); - if ($persistentToken->getUserIdentifier() !== $rememberMeDetails->getUserIdentifier() || $persistentToken->getClass() !== $rememberMeDetails->getUserFqcn()) { + if ($token->getUserIdentifier() !== $rememberMeDetails->getUserIdentifier()) { throw new AuthenticationException('The cookie\'s hash is invalid.'); } @@ -75,41 +75,41 @@ public function consumeRememberMeCookie(RememberMeDetails $rememberMeDetails): U unset($rememberMeDetails); if ($this->tokenVerifier) { - $isTokenValid = $this->tokenVerifier->verifyToken($persistentToken, $tokenValue); + $isTokenValid = $this->tokenVerifier->verifyToken($token, $tokenValue); } else { - $isTokenValid = hash_equals($persistentToken->getTokenValue(), $tokenValue); + $isTokenValid = hash_equals($token->getTokenValue(), $tokenValue); } if (!$isTokenValid) { throw new CookieTheftException('This token was already used. The account is possibly compromised.'); } - $expires = $persistentToken->getLastUsed()->getTimestamp() + $this->options['lifetime']; + $expires = $token->getLastUsed()->getTimestamp() + $this->options['lifetime']; if ($expires < time()) { throw new AuthenticationException('The cookie has expired.'); } return parent::consumeRememberMeCookie(new RememberMeDetails( - $persistentToken->getClass(), - $persistentToken->getUserIdentifier(), + method_exists($token, 'getClass') ? $token->getClass(false) : '', + $token->getUserIdentifier(), $expires, - $persistentToken->getLastUsed()->getTimestamp().':'.$series.':'.$tokenValue.':'.$persistentToken->getClass() + $token->getLastUsed()->getTimestamp().':'.$series.':'.$tokenValue.':'.(method_exists($token, 'getClass') ? $token->getClass(false) : '') )); } public function processRememberMe(RememberMeDetails $rememberMeDetails, UserInterface $user): void { [$lastUsed, $series, $tokenValue, $class] = explode(':', $rememberMeDetails->getValue(), 4); - $persistentToken = new PersistentToken($class, $rememberMeDetails->getUserIdentifier(), $series, $tokenValue, new \DateTimeImmutable('@'.$lastUsed)); + $token = new PersistentToken($class, $rememberMeDetails->getUserIdentifier(), $series, $tokenValue, new \DateTimeImmutable('@'.$lastUsed)); // if a token was regenerated less than a minute ago, there is no need to regenerate it // if multiple concurrent requests reauthenticate a user we do not want to update the token several times - if ($persistentToken->getLastUsed()->getTimestamp() + 60 >= time()) { + if ($token->getLastUsed()->getTimestamp() + 60 >= time()) { return; } $tokenValue = strtr(base64_encode(random_bytes(33)), '+/=', '-_~'); $tokenLastUsed = new \DateTime(); - $this->tokenVerifier?->updateExistingToken($persistentToken, $tokenValue, $tokenLastUsed); + $this->tokenVerifier?->updateExistingToken($token, $tokenValue, $tokenLastUsed); $this->tokenProvider->updateToken($series, $tokenValue, $tokenLastUsed); $this->createCookie($rememberMeDetails->withValue($series.':'.$tokenValue)); diff --git a/src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php b/src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php index 66f9a8f6731c9..7a4a4fc58e30e 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php +++ b/src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php @@ -46,9 +46,9 @@ public static function fromRawCookie(string $rawCookie): self return new static(...$cookieParts); } - public static function fromPersistentToken(PersistentToken $persistentToken, int $expires): self + public static function fromPersistentToken(PersistentToken $token, int $expires): self { - return new static($persistentToken->getClass(), $persistentToken->getUserIdentifier(), $expires, $persistentToken->getSeries().':'.$persistentToken->getTokenValue()); + return new static(method_exists($token, 'getClass') ? $token->getClass(false) : '', $token->getUserIdentifier(), $expires, $token->getSeries().':'.$token->getTokenValue()); } public function withValue(string $value): self @@ -59,8 +59,13 @@ public function withValue(string $value): self return $details; } + /** + * @deprecated since Symfony 7.4, the user FQCN will be removed from the remember-me cookie in 8.0 + */ public function getUserFqcn(): string { + trigger_deprecation('symfony/security-http', '7.4', 'The "%s()" method is deprecated: the user FQCN will be removed from the remember-me cookie in 8.0.', __METHOD__); + return $this->userFqcn; } diff --git a/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php index a4bbe4d0ca427..743f213f1bb12 100644 --- a/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php @@ -50,9 +50,7 @@ public function testCreateRememberMeCookie() { $this->tokenProvider->expects($this->once()) ->method('createNewToken') - ->with($this->callback(fn ($persistentToken) => $persistentToken instanceof PersistentToken - && 'wouter' === $persistentToken->getUserIdentifier() - && InMemoryUser::class === $persistentToken->getClass())); + ->with($this->callback(fn ($token) => $token instanceof PersistentToken && 'wouter' === $token->getUserIdentifier())); $this->handler->createRememberMeCookie(new InMemoryUser('wouter', null)); }