Skip to content

Commit ab5303a

Browse files
committed
Add CacheTokenVerifier and ability to configure a token_verifier on the remember_me security config
1 parent 05afb3b commit ab5303a

File tree

7 files changed

+98
-11
lines changed

7 files changed

+98
-11
lines changed

src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,6 @@ public function verifyToken(PersistentTokenInterface $token, string $tokenValue)
179179
*/
180180
public function updateExistingToken(PersistentTokenInterface $token, string $tokenValue, \DateTimeInterface $lastUsed): void
181181
{
182-
// Update the series with the new token value
183-
$this->updateToken($token->getSeries(), $tokenValue, $lastUsed);
184-
185182
if (!$token instanceof PersistentToken) {
186183
return;
187184
}

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,12 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal
116116
->addTag('security.remember_me_handler', ['firewall' => $firewallName]);
117117
} elseif (isset($config['token_provider'])) {
118118
$tokenProviderId = $this->createTokenProvider($container, $firewallName, $config['token_provider']);
119+
$tokenVerifierId = $config['token_verifier'] ?? null;
119120
$container->setDefinition($rememberMeHandlerId, new ChildDefinition('security.authenticator.persistent_remember_me_handler'))
120121
->replaceArgument(0, new Reference($tokenProviderId))
121122
->replaceArgument(2, new Reference($userProviderId))
122123
->replaceArgument(4, $config)
124+
->replaceArgument(6, $tokenVerifierId ? new Reference($tokenVerifierId) : null)
123125
->addTag('security.remember_me_handler', ['firewall' => $firewallName]);
124126
} else {
125127
$signatureHasherId = 'security.authenticator.remember_me_signature_hasher.'.$firewallName;
@@ -214,6 +216,14 @@ public function addConfiguration(NodeDefinition $node)
214216
->end()
215217
->end()
216218
->end()
219+
->end()
220+
->arrayNode('token_verifier')
221+
->beforeNormalization()
222+
->ifString()->then(function ($v) { return ['service' => $v]; })
223+
->end()
224+
->children()
225+
->scalarNode('service')->info('The service ID of a custom rememberme token verifier.')->end()
226+
->end()
217227
->end();
218228

219229
foreach ($this->options as $name => $value) {

src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@
350350
<xsd:attribute name="secret" type="xsd:string" use="required" />
351351
<xsd:attribute name="service" type="xsd:string" />
352352
<xsd:attribute name="token-provider" type="xsd:string" />
353+
<xsd:attribute name="token-verifier" type="xsd:string" />
353354
<xsd:attribute name="catch-exceptions" type="xsd:boolean" />
354355
<xsd:attribute name="secure" type="remember_me_secure" />
355356
<xsd:attribute name="samesite" type="remember_me_samesite" />

src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_remember_me.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
service('request_stack'),
5252
abstract_arg('options'),
5353
service('logger')->nullOnInvalid(),
54+
abstract_arg('token verifier'),
5455
])
5556
->tag('monolog.logger', ['channel' => 'security'])
5657

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Authentication\RememberMe;
13+
14+
use Psr\Cache\CacheItemPoolInterface;
15+
16+
/**
17+
* @author Jordi Boggiano <j.boggiano@seld.be>
18+
*/
19+
class CacheTokenVerifier implements TokenVerifierInterface
20+
{
21+
/** @var CacheItemPoolInterface */
22+
private $cache;
23+
/** @var int */
24+
private $outdatedTokenTtl;
25+
/** @var string */
26+
private $cacheKeyPrefix;
27+
28+
/**
29+
* @param int $outdatedTokenTtl How long should the outdated token be valid, defaults to 60
30+
* which matches how often the PersistentRememberMeHandler will
31+
* at most refresh tokens. Increasing to more than that is not
32+
* recommended, but you may use a lower value.
33+
*/
34+
public function __construct(CacheItemPoolInterface $cache, int $outdatedTokenTtl = 60, string $cacheKeyPrefix = 'rememberme-')
35+
{
36+
$this->cache = $cache;
37+
$this->outdatedTokenTtl = $outdatedTokenTtl;
38+
}
39+
40+
/**
41+
* {@inheritDoc}
42+
*/
43+
public function verifyToken(PersistentTokenInterface $token, string $tokenValue): bool
44+
{
45+
if (hash_equals($token->getTokenValue(), $tokenValue)) {
46+
return true;
47+
}
48+
49+
if (!$this->cache->hasItem($this->cacheKeyPrefix.$token->getSeries())) {
50+
return false;
51+
}
52+
53+
$item = $this->cache->getItem($this->cacheKeyPrefix.$token->getSeries());
54+
$outdatedToken = $item->get();
55+
56+
return hash_equals($outdatedToken, $tokenValue);
57+
}
58+
59+
/**
60+
* {@inheritDoc}
61+
*/
62+
public function updateExistingToken(PersistentTokenInterface $token, string $tokenValue, \DateTimeInterface $lastUsed): void
63+
{
64+
// When a token gets updated, persist the outdated token for $outdatedTokenTtl seconds so we can
65+
// still accept it as valid in verifyToken
66+
$item = $this->cache->getItem($this->cacheKeyPrefix.$token->getSeries());
67+
$item->set($token->getTokenValue());
68+
$item->expiresAfter($this->outdatedTokenTtl);
69+
$this->cache->save($item);
70+
}
71+
}

src/Symfony/Component/Security/Core/Authentication/RememberMe/TokenVerifierInterface.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
interface TokenVerifierInterface
1818
{
1919
/**
20-
* Verifies that the given $token is valid
20+
* Verifies that the given $token is valid.
2121
*
2222
* This lets you override the token check logic to for example accept slightly outdated tokens.
2323
*
@@ -26,7 +26,7 @@ interface TokenVerifierInterface
2626
public function verifyToken(PersistentTokenInterface $token, string $tokenValue): bool;
2727

2828
/**
29-
* Updates an existing token with a new token value and lastUsed time
29+
* Updates an existing token with a new token value and lastUsed time.
3030
*/
3131
public function updateExistingToken(PersistentTokenInterface $token, string $tokenValue, \DateTimeInterface $lastUsed): void;
3232
}

src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,19 @@
3333
final class PersistentRememberMeHandler extends AbstractRememberMeHandler
3434
{
3535
private $tokenProvider;
36+
/** @var ?TokenVerifierInterface */
37+
private $tokenVerifier;
3638
private $secret;
3739

38-
public function __construct(TokenProviderInterface $tokenProvider, string $secret, UserProviderInterface $userProvider, RequestStack $requestStack, array $options, ?LoggerInterface $logger = null)
40+
public function __construct(TokenProviderInterface $tokenProvider, string $secret, UserProviderInterface $userProvider, RequestStack $requestStack, array $options, ?LoggerInterface $logger = null, ?TokenVerifierInterface $tokenVerifier = null)
3941
{
4042
parent::__construct($userProvider, $requestStack, $options, $logger);
4143

44+
if (!$tokenVerifier && $tokenProvider instanceof TokenVerifierInterface) {
45+
$tokenVerifier = $tokenProvider;
46+
}
4247
$this->tokenProvider = $tokenProvider;
48+
$this->tokenVerifier = $tokenVerifier;
4349
$this->secret = $secret;
4450
}
4551

@@ -69,7 +75,7 @@ public function processRememberMe(RememberMeDetails $rememberMeDetails, UserInte
6975
$persistentToken = $this->tokenProvider->loadTokenBySeries($series);
7076

7177
if (
72-
($this->tokenProvider instanceof TokenVerifierInterface && !$this->tokenProvider->verifyToken($persistentToken, $tokenValue))
78+
($this->tokenVerifier && !$this->tokenVerifier->verifyToken($persistentToken, $tokenValue))
7379
|| !hash_equals($persistentToken->getTokenValue(), $tokenValue)
7480
) {
7581
throw new CookieTheftException('This token was already used. The account is possibly compromised.');
@@ -83,11 +89,12 @@ public function processRememberMe(RememberMeDetails $rememberMeDetails, UserInte
8389
// if multiple concurrent requests reauthenticate a user we do not want to update the token several times
8490
if ($persistentToken->getLastUsed()->getTimestamp() + 60 < time()) {
8591
$tokenValue = base64_encode(random_bytes(64));
86-
if ($this->tokenProvider instanceof TokenVerifierInterface) {
87-
$this->tokenProvider->updateExistingToken($persistentToken, $this->generateHash($tokenValue), new \DateTime());
88-
} else {
89-
$this->tokenProvider->updateToken($series, $this->generateHash($tokenValue), new \DateTime());
92+
$tokenValueHash = $this->generateHash($tokenValue);
93+
$tokenLastUsed = new \DateTime();
94+
if ($this->tokenVerifier) {
95+
$this->tokenVerifier->updateExistingToken($persistentToken, $tokenValueHash, $tokenLastUsed);
9096
}
97+
$this->tokenProvider->updateToken($series, $tokenValueHash, $tokenLastUsed);
9198
}
9299

93100
$this->createCookie($rememberMeDetails->withValue($tokenValue));

0 commit comments

Comments
 (0)