Skip to content

Commit d296685

Browse files
Seldaekfabpot
authored andcommitted
[Security] [RememberMe] Add support for parallel requests doing remember-me re-authentication
1 parent d70084b commit d296685

File tree

4 files changed

+145
-0
lines changed

4 files changed

+145
-0
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
private $cache;
22+
private $outdatedTokenTtl;
23+
private $cacheKeyPrefix;
24+
25+
/**
26+
* @param int $outdatedTokenTtl How long the outdated token should still be considered valid. Defaults
27+
* to 60, which matches how often the PersistentRememberMeHandler will at
28+
* most refresh tokens. Increasing to more than that is not recommended,
29+
* but you may use a lower value.
30+
*/
31+
public function __construct(CacheItemPoolInterface $cache, int $outdatedTokenTtl = 60, string $cacheKeyPrefix = 'rememberme-stale-')
32+
{
33+
$this->cache = $cache;
34+
$this->outdatedTokenTtl = $outdatedTokenTtl;
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function verifyToken(PersistentTokenInterface $token, string $tokenValue): bool
41+
{
42+
if (hash_equals($token->getTokenValue(), $tokenValue)) {
43+
return true;
44+
}
45+
46+
if (!$this->cache->hasItem($this->cacheKeyPrefix.$token->getSeries())) {
47+
return false;
48+
}
49+
50+
$item = $this->cache->getItem($this->cacheKeyPrefix.$token->getSeries());
51+
$outdatedToken = $item->get();
52+
53+
return hash_equals($outdatedToken, $tokenValue);
54+
}
55+
56+
/**
57+
* {@inheritdoc}
58+
*/
59+
public function updateExistingToken(PersistentTokenInterface $token, string $tokenValue, \DateTimeInterface $lastUsed): void
60+
{
61+
// When a token gets updated, persist the outdated token for $outdatedTokenTtl seconds so we can
62+
// still accept it as valid in verifyToken
63+
$item = $this->cache->getItem($this->cacheKeyPrefix.$token->getSeries());
64+
$item->set($token->getTokenValue());
65+
$item->expiresAfter($this->outdatedTokenTtl);
66+
$this->cache->save($item);
67+
}
68+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
/**
15+
* @author Jordi Boggiano <j.boggiano@seld.be>
16+
*/
17+
interface TokenVerifierInterface
18+
{
19+
/**
20+
* Verifies that the given $token is valid.
21+
*
22+
* This lets you override the token check logic to for example accept slightly outdated tokens.
23+
*
24+
* Do not forget to implement token comparisons using hash_equals for a secure implementation.
25+
*/
26+
public function verifyToken(PersistentTokenInterface $token, string $tokenValue): bool;
27+
28+
/**
29+
* Updates an existing token with a new token value and lastUsed time.
30+
*/
31+
public function updateExistingToken(PersistentTokenInterface $token, string $tokenValue, \DateTimeInterface $lastUsed): void;
32+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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\Tests\Authentication\RememberMe;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
16+
use Symfony\Component\Security\Core\Authentication\RememberMe\CacheTokenVerifier;
17+
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken;
18+
19+
class CacheTokenVerifierTest extends TestCase
20+
{
21+
public function testVerifyCurrentToken()
22+
{
23+
$verifier = new CacheTokenVerifier(new ArrayAdapter());
24+
$token = new PersistentToken('class', 'user', 'series1', 'value', new \DateTime());
25+
$this->assertTrue($verifier->verifyToken($token, 'value'));
26+
}
27+
28+
public function testVerifyFailsInvalidToken()
29+
{
30+
$verifier = new CacheTokenVerifier(new ArrayAdapter());
31+
$token = new PersistentToken('class', 'user', 'series1', 'value', new \DateTime());
32+
$this->assertFalse($verifier->verifyToken($token, 'wrong-value'));
33+
}
34+
35+
public function testVerifyOutdatedToken()
36+
{
37+
$verifier = new CacheTokenVerifier(new ArrayAdapter());
38+
$outdatedToken = new PersistentToken('class', 'user', 'series1', 'value', new \DateTime());
39+
$newToken = new PersistentToken('class', 'user', 'series1', 'newvalue', new \DateTime());
40+
$verifier->updateExistingToken($outdatedToken, 'newvalue', new \DateTime());
41+
$this->assertTrue($verifier->verifyToken($newToken, 'value'));
42+
}
43+
}

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
},
2626
"require-dev": {
2727
"psr/container": "^1.0|^2.0",
28+
"psr/cache": "^1.0|^2.0|^3.0",
29+
"symfony/cache": "^4.4|^5.0",
2830
"symfony/event-dispatcher": "^4.4|^5.0",
2931
"symfony/expression-language": "^4.4|^5.0",
3032
"symfony/http-foundation": "^5.3",

0 commit comments

Comments
 (0)