Skip to content

Commit d7e6541

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

File tree

7 files changed

+94
-11
lines changed

7 files changed

+94
-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: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
26+
/**
27+
* @param int $outdatedTokenTtl How long should the outdated token be valid, defaults to 60
28+
* which matches how often the PersistentRememberMeHandler will
29+
* at most refresh tokens. Increasing to more than that is not
30+
* recommended, but you may use a lower value.
31+
*/
32+
public function __construct(CacheItemPoolInterface $cache, int $outdatedTokenTtl = 60)
33+
{
34+
$this->cache = $cache;
35+
$this->outdatedTokenTtl = $outdatedTokenTtl;
36+
}
37+
38+
/**
39+
* {@inheritDoc}
40+
*/
41+
public function verifyToken(PersistentTokenInterface $token, string $tokenValue): bool
42+
{
43+
if (hash_equals($token->getTokenValue(), $tokenValue)) {
44+
return true;
45+
}
46+
47+
if (!$this->cache->hasItem('rememberme-' . $token->getSeries())) {
48+
return false;
49+
}
50+
51+
$item = $this->cache->getItem('rememberme-' . $token->getSeries());
52+
$oldToken = $item->get();
53+
54+
return hash_equals($oldToken, $tokenValue);
55+
}
56+
57+
/**
58+
* {@inheritDoc}
59+
*/
60+
public function updateExistingToken(PersistentTokenInterface $token, string $tokenValue, \DateTimeInterface $lastUsed): void
61+
{
62+
$item = $this->cache->getItem(sprintf(self::CACHE_KEY, $this->oldRememberMeToken));
63+
$item->set($newRememberMeToken);
64+
$item->expiresAfter($this->outdatedTokenTtl);
65+
$this->cache->save($item);
66+
}
67+
}

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy