* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Security\Http\RememberMe; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken; use Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface; use Symfony\Component\Security\Core\Authentication\RememberMe\TokenVerifierInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\CookieTheftException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; /** * Implements remember-me tokens using a {@see TokenProviderInterface}. * * This requires storing remember-me tokens in a database. This allows * more control over the invalidation of remember-me tokens. See * {@see SignatureRememberMeHandler} if you don't want to use a database. * * @author Wouter de Jong */ final class PersistentRememberMeHandler extends AbstractRememberMeHandler { private TokenProviderInterface $tokenProvider; private ?TokenVerifierInterface $tokenVerifier; public function __construct(TokenProviderInterface $tokenProvider, UserProviderInterface $userProvider, RequestStack $requestStack, array $options, ?LoggerInterface $logger = null, ?TokenVerifierInterface $tokenVerifier = null) { parent::__construct($userProvider, $requestStack, $options, $logger); if (!$tokenVerifier && $tokenProvider instanceof TokenVerifierInterface) { $tokenVerifier = $tokenProvider; } $this->tokenProvider = $tokenProvider; $this->tokenVerifier = $tokenVerifier; } public function createRememberMeCookie(UserInterface $user): void { $series = random_bytes(66); $tokenValue = strtr(base64_encode(substr($series, 33)), '+/=', '-_~'); $series = strtr(base64_encode(substr($series, 0, 33)), '+/=', '-_~'); $token = new PersistentToken($user::class, $user->getUserIdentifier(), $series, $tokenValue, new \DateTimeImmutable()); $this->tokenProvider->createNewToken($token); $this->createCookie(RememberMeDetails::fromPersistentToken($token, time() + $this->options['lifetime'])); } public function consumeRememberMeCookie(RememberMeDetails $rememberMeDetails): UserInterface { if (!str_contains($rememberMeDetails->getValue(), ':')) { throw new AuthenticationException('The cookie is incorrectly formatted.'); } [$series, $tokenValue] = explode(':', $rememberMeDetails->getValue()); $persistentToken = $this->tokenProvider->loadTokenBySeries($series); if ($this->tokenVerifier) { $isTokenValid = $this->tokenVerifier->verifyToken($persistentToken, $tokenValue); } else { $isTokenValid = hash_equals($persistentToken->getTokenValue(), $tokenValue); } if (!$isTokenValid) { throw new CookieTheftException('This token was already used. The account is possibly compromised.'); } if ($persistentToken->getLastUsed()->getTimestamp() + $this->options['lifetime'] < time()) { throw new AuthenticationException('The cookie has expired.'); } return parent::consumeRememberMeCookie($rememberMeDetails->withValue($persistentToken->getLastUsed()->getTimestamp().':'.$rememberMeDetails->getValue().':'.$persistentToken->getClass())); } 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)); // 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()) { return; } $tokenValue = strtr(base64_encode(random_bytes(33)), '+/=', '-_~'); $tokenLastUsed = new \DateTime(); $this->tokenVerifier?->updateExistingToken($persistentToken, $tokenValue, $tokenLastUsed); $this->tokenProvider->updateToken($series, $tokenValue, $tokenLastUsed); $this->createCookie($rememberMeDetails->withValue($series.':'.$tokenValue)); } public function clearRememberMeCookie(): void { parent::clearRememberMeCookie(); $cookie = $this->requestStack->getMainRequest()->cookies->get($this->options['name']); if (null === $cookie) { return; } $rememberMeDetails = RememberMeDetails::fromRawCookie($cookie); [$series] = explode(':', $rememberMeDetails->getValue()); $this->tokenProvider->deleteTokenBySeries($series); } /** * @internal */ public function getTokenProvider(): TokenProviderInterface { return $this->tokenProvider; } }