Skip to content

Commit 95f22dd

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

File tree

3 files changed

+112
-1
lines changed

3 files changed

+112
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
* Deprecate `DoctrineTestHelper` and `TestRepositoryFactory`
99
* [BC BREAK] Remove `UuidV*Generator` classes
1010
* Add `UuidGenerator`
11+
* Add support for the new security-core `TokenVerifierInterface` in `DoctrineTokenProvider`, fixing parallel requests handling in remember-me
1112

1213
5.2.0
1314
-----

Security/RememberMe/DoctrineTokenProvider.php

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken;
2020
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentTokenInterface;
2121
use Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface;
22+
use Symfony\Component\Security\Core\Authentication\RememberMe\TokenVerifierInterface;
2223
use Symfony\Component\Security\Core\Exception\TokenNotFoundException;
2324

2425
/**
@@ -39,7 +40,7 @@
3940
* `username` varchar(200) NOT NULL
4041
* );
4142
*/
42-
class DoctrineTokenProvider implements TokenProviderInterface
43+
class DoctrineTokenProvider implements TokenProviderInterface, TokenVerifierInterface
4344
{
4445
private $conn;
4546

@@ -136,6 +137,65 @@ public function createNewToken(PersistentTokenInterface $token)
136137
}
137138
}
138139

140+
/**
141+
* {@inheritdoc}
142+
*/
143+
public function verifyToken(PersistentTokenInterface $token, string $tokenValue): bool
144+
{
145+
// Check if the token value matches the current persisted token
146+
if (hash_equals($token->getTokenValue(), $tokenValue)) {
147+
return true;
148+
}
149+
150+
// Generate an alternative series id here by changing the suffix == to _
151+
// this is needed to be able to store an older token value in the database
152+
// which has a PRIMARY(series), and it works as long as series ids are
153+
// generated using base64_encode(random_bytes(64)) which always outputs
154+
// a == suffix, but if it should not work for some reason we abort
155+
// for safety
156+
$tmpSeries = preg_replace('{=+$}', '_', $token->getSeries());
157+
if ($tmpSeries === $token->getSeries()) {
158+
return false;
159+
}
160+
161+
// Check if the previous token is present. If the given $tokenValue
162+
// matches the previous token (and it is outdated by at most 60seconds)
163+
// we also accept it as a valid value.
164+
try {
165+
$tmpToken = $this->loadTokenBySeries($tmpSeries);
166+
} catch (TokenNotFoundException $e) {
167+
return false;
168+
}
169+
170+
if ($tmpToken->getLastUsed()->getTimestamp() + 60 < time()) {
171+
return false;
172+
}
173+
174+
return hash_equals($tmpToken->getTokenValue(), $tokenValue);
175+
}
176+
177+
/**
178+
* {@inheritdoc}
179+
*/
180+
public function updateExistingToken(PersistentTokenInterface $token, string $tokenValue, \DateTimeInterface $lastUsed): void
181+
{
182+
if (!$token instanceof PersistentToken) {
183+
return;
184+
}
185+
186+
// Persist a copy of the previous token for authentication
187+
// in verifyToken should the old token still be sent by the browser
188+
// in a request concurrent to the one that did this token update
189+
$tmpSeries = preg_replace('{=+$}', '_', $token->getSeries());
190+
// if we cannot generate a unique series it is not worth trying further
191+
if ($tmpSeries === $token->getSeries()) {
192+
return;
193+
}
194+
195+
$this->deleteTokenBySeries($tmpSeries);
196+
$this->createNewToken(new PersistentToken($token->getClass(), $token->getUserIdentifier(), $tmpSeries, $token->getTokenValue(), $lastUsed));
197+
}
198+
139199
/**
140200
* Adds the Table to the Schema if "remember me" uses this Connection.
141201
*/

Tests/Security/RememberMe/DoctrineTokenProviderTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,56 @@ public function testDeleteToken()
5656
$provider->loadTokenBySeries('someSeries');
5757
}
5858

59+
public function testVerifyOutdatedTokenAfterParallelRequest()
60+
{
61+
$provider = $this->bootstrapProvider();
62+
$series = base64_encode(random_bytes(64));
63+
$oldValue = 'oldValue';
64+
$newValue = 'newValue';
65+
66+
// setup existing token
67+
$token = new PersistentToken('someClass', 'someUser', $series, $oldValue, new \DateTime('2013-01-26T18:23:51'));
68+
$provider->createNewToken($token);
69+
70+
// new request comes in requiring remember-me auth, which updates the token
71+
$provider->updateExistingToken($token, $newValue, new \DateTime('-5 seconds'));
72+
$provider->updateToken($series, $newValue, new \DateTime('-5 seconds'));
73+
74+
// parallel request comes in with the old remember-me cookie and session, which also requires reauth
75+
$token = $provider->loadTokenBySeries($series);
76+
$this->assertEquals($newValue, $token->getTokenValue());
77+
78+
// new token is valid
79+
$this->assertTrue($provider->verifyToken($token, $newValue));
80+
// old token is still valid
81+
$this->assertTrue($provider->verifyToken($token, $oldValue));
82+
}
83+
84+
public function testVerifyOutdatedTokenAfterParallelRequestFailsAfter60Seconds()
85+
{
86+
$provider = $this->bootstrapProvider();
87+
$series = base64_encode(random_bytes(64));
88+
$oldValue = 'oldValue';
89+
$newValue = 'newValue';
90+
91+
// setup existing token
92+
$token = new PersistentToken('someClass', 'someUser', $series, $oldValue, new \DateTime('2013-01-26T18:23:51'));
93+
$provider->createNewToken($token);
94+
95+
// new request comes in requiring remember-me auth, which updates the token
96+
$provider->updateExistingToken($token, $newValue, new \DateTime('-61 seconds'));
97+
$provider->updateToken($series, $newValue, new \DateTime('-5 seconds'));
98+
99+
// parallel request comes in with the old remember-me cookie and session, which also requires reauth
100+
$token = $provider->loadTokenBySeries($series);
101+
$this->assertEquals($newValue, $token->getTokenValue());
102+
103+
// new token is valid
104+
$this->assertTrue($provider->verifyToken($token, $newValue));
105+
// old token is not valid anymore after 60 seconds
106+
$this->assertFalse($provider->verifyToken($token, $oldValue));
107+
}
108+
59109
/**
60110
* @return DoctrineTokenProvider
61111
*/

0 commit comments

Comments
 (0)