Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Security] Make PersistentToken immutable and tell TokenProviderInterface::updateToken() implementations should accept DateTimeInterface #50290

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions UPGRADE-6.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,5 @@ Security
--------

* `UserValueResolver` no longer implements `ArgumentValueResolverInterface`
* Make `PersistentToken` immutable
* Deprecate accepting only `DateTime` for `TokenProviderInterface::updateToken()`, use `DateTimeInterface` instead
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
* `class` varchar(100) NOT NULL,
* `username` varchar(200) NOT NULL
* );
*
* @final since Symfony 6.4
*/
class DoctrineTokenProvider implements TokenProviderInterface, TokenVerifierInterface
{
Expand All @@ -60,7 +62,7 @@ public function loadTokenBySeries(string $series): PersistentTokenInterface
$row = $stmt instanceof Result || $stmt instanceof DriverResult ? $stmt->fetchAssociative() : $stmt->fetch(\PDO::FETCH_ASSOC);

if ($row) {
return new PersistentToken($row['class'], $row['username'], $series, $row['value'], new \DateTime($row['last_used']));
return new PersistentToken($row['class'], $row['username'], $series, $row['value'], new \DateTimeImmutable($row['last_used']));
}

throw new TokenNotFoundException('No token found.');
Expand All @@ -82,6 +84,8 @@ public function deleteTokenBySeries(string $series)
}

/**
* @param \DateTimeInterface $lastUsed Accepting only DateTime is deprecated since Symfony 6.4
*
* @return void
*/
public function updateToken(string $series, #[\SensitiveParameter] string $tokenValue, \DateTime $lastUsed)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public function testCreateNewToken()
{
$provider = $this->bootstrapProvider();

$token = new PersistentToken('someClass', 'someUser', 'someSeries', 'tokenValue', new \DateTime('2013-01-26T18:23:51'));
$token = new PersistentToken('someClass', 'someUser', 'someSeries', 'tokenValue', new \DateTimeImmutable('2013-01-26T18:23:51'));
$provider->createNewToken($token);

$this->assertEquals($provider->loadTokenBySeries('someSeries'), $token);
Expand All @@ -47,7 +47,7 @@ public function testUpdateToken()
{
$provider = $this->bootstrapProvider();

$token = new PersistentToken('someClass', 'someUser', 'someSeries', 'tokenValue', new \DateTime('2013-01-26T18:23:51'));
$token = new PersistentToken('someClass', 'someUser', 'someSeries', 'tokenValue', new \DateTimeImmutable('2013-01-26T18:23:51'));
$provider->createNewToken($token);
$provider->updateToken('someSeries', 'newValue', $lastUsed = new \DateTime('2014-06-26T22:03:46'));
$token = $provider->loadTokenBySeries('someSeries');
Expand All @@ -59,7 +59,7 @@ public function testUpdateToken()
public function testDeleteToken()
{
$provider = $this->bootstrapProvider();
$token = new PersistentToken('someClass', 'someUser', 'someSeries', 'tokenValue', new \DateTime('2013-01-26T18:23:51'));
$token = new PersistentToken('someClass', 'someUser', 'someSeries', 'tokenValue', new \DateTimeImmutable('2013-01-26T18:23:51'));
$provider->createNewToken($token);
$provider->deleteTokenBySeries('someSeries');

Expand All @@ -76,7 +76,7 @@ public function testVerifyOutdatedTokenAfterParallelRequest()
$newValue = 'newValue';

// setup existing token
$token = new PersistentToken('someClass', 'someUser', $series, $oldValue, new \DateTime('2013-01-26T18:23:51'));
$token = new PersistentToken('someClass', 'someUser', $series, $oldValue, new \DateTimeImmutable('2013-01-26T18:23:51'));
$provider->createNewToken($token);

// new request comes in requiring remember-me auth, which updates the token
Expand All @@ -101,7 +101,7 @@ public function testVerifyOutdatedTokenAfterParallelRequestFailsAfter60Seconds()
$newValue = 'newValue';

// setup existing token
$token = new PersistentToken('someClass', 'someUser', $series, $oldValue, new \DateTime('2013-01-26T18:23:51'));
$token = new PersistentToken('someClass', 'someUser', $series, $oldValue, new \DateTimeImmutable('2013-01-26T18:23:51'));
$provider->createNewToken($token);

// new request comes in requiring remember-me auth, which updates the token
Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Bridge/Doctrine/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"symfony/property-access": "^5.4|^6.0|^7.0",
"symfony/property-info": "^5.4|^6.0|^7.0",
"symfony/proxy-manager-bridge": "^5.4|^6.0|^7.0",
"symfony/security-core": "^6.0|^7.0",
"symfony/security-core": "^6.4|^7.0",
"symfony/stopwatch": "^5.4|^6.0|^7.0",
"symfony/translation": "^5.4|^6.0|^7.0",
"symfony/uid": "^5.4|^6.0|^7.0",
Expand All @@ -64,7 +64,7 @@
"symfony/messenger": "<5.4",
"symfony/property-info": "<5.4",
"symfony/security-bundle": "<5.4",
"symfony/security-core": "<6.0",
"symfony/security-core": "<6.4",
"symfony/validator": "<5.4.25|>=6,<6.2.12|>=6.3,<6.3.1"
},
"autoload": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
* This class is used for testing purposes, and is not really suited for production.
*
* @author Johannes M. Schmitt <[email protected]>
*
* @final since Symfony 6.4
*/
class InMemoryTokenProvider implements TokenProviderInterface
{
Expand All @@ -32,6 +34,8 @@ public function loadTokenBySeries(string $series): PersistentTokenInterface
}

/**
* @param \DateTimeInterface $lastUsed Accepting only DateTime is deprecated since Symfony 6.4
*
* @return void
*/
public function updateToken(string $series, #[\SensitiveParameter] string $tokenValue, \DateTime $lastUsed)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ final class PersistentToken implements PersistentTokenInterface
private string $userIdentifier;
private string $series;
private string $tokenValue;
private \DateTime $lastUsed;
private \DateTimeImmutable $lastUsed;

public function __construct(string $class, string $userIdentifier, string $series, #[\SensitiveParameter] string $tokenValue, \DateTime $lastUsed)
public function __construct(string $class, string $userIdentifier, string $series, #[\SensitiveParameter] string $tokenValue, \DateTimeInterface $lastUsed)
{
if (empty($class)) {
throw new \InvalidArgumentException('$class must not be empty.');
Expand All @@ -43,7 +43,7 @@ public function __construct(string $class, string $userIdentifier, string $serie
$this->userIdentifier = $userIdentifier;
$this->series = $series;
$this->tokenValue = $tokenValue;
$this->lastUsed = $lastUsed;
$this->lastUsed = \DateTimeImmutable::createFromInterface($lastUsed);
}

public function getClass(): string
Expand All @@ -68,6 +68,6 @@ public function getTokenValue(): string

public function getLastUsed(): \DateTime
{
return $this->lastUsed;
return \DateTime::createFromImmutable($this->lastUsed);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public function getTokenValue(): string;

/**
* Returns the time the token was last used.
*
* Each call SHOULD return a new distinct DateTime instance.
*/
public function getLastUsed(): \DateTime;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public function deleteTokenBySeries(string $series);
/**
* Updates the token according to this data.
*
* @param \DateTimeInterface $lastUsed Accepting only DateTime is deprecated since Symfony 6.4
*
* @return void
*
* @throws TokenNotFoundException if the token is not found
Expand Down
6 changes: 6 additions & 0 deletions src/Symfony/Component/Security/Core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
CHANGELOG
=========

6.4
---

* Make `PersistentToken` immutable
* Deprecate accepting only `DateTime` for `TokenProviderInterface::updateToken()`, use `DateTimeInterface` instead

6.3
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,23 @@ class CacheTokenVerifierTest extends TestCase
public function testVerifyCurrentToken()
{
$verifier = new CacheTokenVerifier(new ArrayAdapter());
$token = new PersistentToken('class', 'user', 'series1@special:chars=/', 'value', new \DateTime());
$token = new PersistentToken('class', 'user', 'series1@special:chars=/', 'value', new \DateTimeImmutable());
$this->assertTrue($verifier->verifyToken($token, 'value'));
}

public function testVerifyFailsInvalidToken()
{
$verifier = new CacheTokenVerifier(new ArrayAdapter());
$token = new PersistentToken('class', 'user', 'series1@special:chars=/', 'value', new \DateTime());
$token = new PersistentToken('class', 'user', 'series1@special:chars=/', 'value', new \DateTimeImmutable());
$this->assertFalse($verifier->verifyToken($token, 'wrong-value'));
}

public function testVerifyOutdatedToken()
{
$verifier = new CacheTokenVerifier(new ArrayAdapter());
$outdatedToken = new PersistentToken('class', 'user', 'series1@special:chars=/', 'value', new \DateTime());
$newToken = new PersistentToken('class', 'user', 'series1@special:chars=/', 'newvalue', new \DateTime());
$verifier->updateExistingToken($outdatedToken, 'newvalue', new \DateTime());
$outdatedToken = new PersistentToken('class', 'user', 'series1@special:chars=/', 'value', new \DateTimeImmutable());
$newToken = new PersistentToken('class', 'user', 'series1@special:chars=/', 'newvalue', new \DateTimeImmutable());
$verifier->updateExistingToken($outdatedToken, 'newvalue', new \DateTimeImmutable());
$this->assertTrue($verifier->verifyToken($newToken, 'value'));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public function testCreateNewToken()
{
$provider = new InMemoryTokenProvider();

$token = new PersistentToken('foo', 'foo', 'foo', 'foo', new \DateTime());
$token = new PersistentToken('foo', 'foo', 'foo', 'foo', new \DateTimeImmutable());
$provider->createNewToken($token);

$this->assertSame($provider->loadTokenBySeries('foo'), $token);
Expand All @@ -39,21 +39,21 @@ public function testUpdateToken()
{
$provider = new InMemoryTokenProvider();

$token = new PersistentToken('foo', 'foo', 'foo', 'foo', new \DateTime());
$token = new PersistentToken('foo', 'foo', 'foo', 'foo', new \DateTimeImmutable());
$provider->createNewToken($token);
$provider->updateToken('foo', 'newFoo', $lastUsed = new \DateTime());
$token = $provider->loadTokenBySeries('foo');

$this->assertEquals('newFoo', $token->getTokenValue());
$this->assertSame($token->getLastUsed(), $lastUsed);
$this->assertEquals($token->getLastUsed(), $lastUsed);
}

public function testDeleteToken()
{
$this->expectException(TokenNotFoundException::class);
$provider = new InMemoryTokenProvider();

$token = new PersistentToken('foo', 'foo', 'foo', 'foo', new \DateTime());
$token = new PersistentToken('foo', 'foo', 'foo', 'foo', new \DateTimeImmutable());
$provider->createNewToken($token);
$provider->deleteTokenBySeries('foo');
$provider->loadTokenBySeries('foo');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,21 @@ class PersistentTokenTest extends TestCase
{
public function testConstructor()
{
$lastUsed = new \DateTime();
$lastUsed = new \DateTimeImmutable();
$token = new PersistentToken('fooclass', 'fooname', 'fooseries', 'footokenvalue', $lastUsed);

$this->assertEquals('fooclass', $token->getClass());
$this->assertEquals('fooname', $token->getUserIdentifier());
$this->assertEquals('fooseries', $token->getSeries());
$this->assertEquals('footokenvalue', $token->getTokenValue());
$this->assertSame($lastUsed, $token->getLastUsed());
$this->assertEquals($lastUsed, $token->getLastUsed());
}

public function testDateTime()
{
$lastUsed = new \DateTime();
$token = new PersistentToken('fooclass', 'fooname', 'fooseries', 'footokenvalue', $lastUsed);

$this->assertEquals($lastUsed, $token->getLastUsed());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ 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 \DateTime());
$token = new PersistentToken($user::class, $user->getUserIdentifier(), $series, $tokenValue, new \DateTimeImmutable());

$this->tokenProvider->createNewToken($token);
$this->createCookie(RememberMeDetails::fromPersistentToken($token, time() + $this->options['lifetime']));
Expand Down Expand Up @@ -122,7 +122,7 @@ public function consumeRememberMeCookie(RememberMeDetails $rememberMeDetails): U
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 \DateTime('@'.$lastUsed));
$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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public function testConsumeRememberMeCookieValid()
$this->tokenProvider->expects($this->any())
->method('loadTokenBySeries')
->with('series1')
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTime('-10 min')))
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTimeImmutable('-10 min')))
;

$this->tokenProvider->expects($this->once())->method('updateToken')->with('series1');
Expand Down Expand Up @@ -106,7 +106,7 @@ public function testConsumeRememberMeCookieValidByValidatorWithoutUpdate()
$verifier = $this->createMock(TokenVerifierInterface::class);
$handler = new PersistentRememberMeHandler($this->tokenProvider, $this->userProvider, $this->requestStack, [], null, $verifier);

$persistentToken = new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTime('30 seconds'));
$persistentToken = new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTimeImmutable('30 seconds'));

$this->tokenProvider->expects($this->any())
->method('loadTokenBySeries')
Expand All @@ -133,7 +133,7 @@ public function testConsumeRememberMeCookieInvalidToken()
$this->tokenProvider->expects($this->any())
->method('loadTokenBySeries')
->with('series1')
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue1', new \DateTime('-10 min')));
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue1', new \DateTimeImmutable('-10 min')));

$this->tokenProvider->expects($this->never())->method('updateToken')->with('series1');

Expand All @@ -148,7 +148,7 @@ public function testConsumeRememberMeCookieExpired()
$this->tokenProvider->expects($this->any())
->method('loadTokenBySeries')
->with('series1')
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTime('@'.(time() - (31536000 + 1)))));
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTimeImmutable('@'.(time() - (31536000 + 1)))));

$this->tokenProvider->expects($this->never())->method('updateToken')->with('series1');

Expand All @@ -160,7 +160,7 @@ public function testBase64EncodedTokens()
$this->tokenProvider->expects($this->any())
->method('loadTokenBySeries')
->with('series1')
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTime('-10 min')))
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTimeImmutable('-10 min')))
;

$this->tokenProvider->expects($this->once())->method('updateToken')->with('series1');
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/Security/Http/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"require": {
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/security-core": "^6.3|^7.0",
"symfony/security-core": "^6.4|^7.0",
"symfony/http-foundation": "^6.2|^7.0",
"symfony/http-kernel": "^6.3|^7.0",
"symfony/polyfill-mbstring": "~1.0",
Expand Down