Skip to content

[Security] Ban \DateTime from Security component #47772

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

Closed
wants to merge 1 commit into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function loadTokenBySeries(string $series): PersistentTokenInterface
return $this->tokens[$series];
}

public function updateToken(string $series, #[\SensitiveParameter] string $tokenValue, \DateTime $lastUsed)
public function updateToken(string $series, #[\SensitiveParameter] string $tokenValue, \DateTimeInterface $lastUsed)
{
if (!isset($this->tokens[$series])) {
throw new TokenNotFoundException('No token found.');
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 \DateTimeInterface $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 @@ -39,6 +39,10 @@ public function __construct(string $class, string $userIdentifier, string $serie
throw new \InvalidArgumentException('$tokenValue must not be empty.');
}

if ($lastUsed instanceof \DateTime) {
trigger_deprecation('symfony/security-core', '6.2', 'usage of \DateTime is deprecated use \DateTimeImmutable instead.');
}

$this->class = $class;
$this->userIdentifier = $userIdentifier;
$this->series = $series;
Expand Down Expand Up @@ -66,7 +70,7 @@ public function getTokenValue(): string
return $this->tokenValue;
}

public function getLastUsed(): \DateTime
public function getLastUsed(): \DateTimeInterface
{
return $this->lastUsed;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public function getTokenValue(): string;
/**
* Returns the time the token was last used.
*/
public function getLastUsed(): \DateTime;
public function getLastUsed(): \DateTimeInterface;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a BC breaks for consumers of the interface

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I try to do here is to use the covariance feature of PHP. I don't get why is it a BC breaks ? If someone consumes this interface with DateTime he still can because DateTime is a child of DateTimeInterface.

Copy link
Member

@wouterj wouterj Oct 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a BC break for the caller, as they could rely on the method returning DateTime before. E.g. this code works before this change, but breaks after:

$lastUsed = $token->getLastUsed();
$lastUsed->modify('+30 minutes');

if ($lastUsed < new \DateTimeImmutable('now')) {
    throw new \RuntimeException('This token was last used 30 minutes ago');
}

(or simpler use-cases e.g. where this is passed in a function requiring DateTime).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The consumer of a return type is not choosing whether it uses DateTime or a DateTimeInterface. It has to handle everything.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok thanks both of you. I understand your point. But I don't see any way to it without leading to a BC break. Maybe we should target the next major version?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also next major versions cannot have BC breaks that didn't have a deprecation (and way to get out of deprecation) in the last minor before it.

We would have to create a new method with the new return type and deprecate the old method. But I'm personally not sure if that is worth is: What about delaying to remove mutable DateTime in user-facing code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolas-grekas I ping you here. I think you should be part of this discussion

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've no perfect solution to this. We would document a way to detect all such uses of DateTime and rewrite them automatically:

Before:

$lastUsed = $token->getLastUsed();
$lastUsed->modify('+30 minutes');

After:

$lastUsed = $token->getLastUsed();
$lastUsed = $lastUsed->modify('+30 minutes');

That'd make the code the same whether DateTime or DateTimeImmutable is used.
Can Rector do that? /cc @TomasVotruba
Should the PHP engine trigger a deprecation when the return value of one of DateTime's setters is not used? I'm not sure how this would be possible technically, but let's just dream a bit :) /cc @derickr

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolas-grekas If the type is known, Rector can handle it :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And regarding the widened type, we might need something like ReturnTypeWillChange to indicate that this interface will change its return type in the future. But there is no userland solution for that.


/**
* Returns the identifier used to authenticate (e.g. their email address or username).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function deleteTokenBySeries(string $series);
*
* @throws TokenNotFoundException if the token is not found
*/
public function updateToken(string $series, #[\SensitiveParameter] string $tokenValue, \DateTime $lastUsed);
public function updateToken(string $series, #[\SensitiveParameter] string $tokenValue, \DateTimeInterface $lastUsed);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a BC break for implementors of the interface

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove the param from 6.2 and replace it with an @paramannotation? I think you said we cannot but I forgot the reasoning once again 😬

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't have a default value, which means removing the param is not allowed as that would mean the implementors are flagged as incompatible with the interface?


/**
* Creates a new token.
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,9 +39,9 @@ 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());
$provider->updateToken('foo', 'newFoo', $lastUsed = new \DateTimeImmutable());
$token = $provider->loadTokenBySeries('foo');

$this->assertEquals('newFoo', $token->getTokenValue());
Expand All @@ -53,7 +53,7 @@ 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,7 +18,7 @@ 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function createRememberMeCookie(UserInterface $user): void
{
$series = base64_encode(random_bytes(64));
$tokenValue = $this->generateHash(base64_encode(random_bytes(64)));
$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 @@ -84,7 +84,7 @@ public function processRememberMe(RememberMeDetails $rememberMeDetails, UserInte
// if multiple concurrent requests reauthenticate a user we do not want to update the token several times
if ($persistentToken->getLastUsed()->getTimestamp() + 60 < time()) {
$tokenValue = $this->generateHash(base64_encode(random_bytes(64)));
$tokenLastUsed = new \DateTime();
$tokenLastUsed = new \DateTimeImmutable();
$this->tokenVerifier?->updateExistingToken($persistentToken, $tokenValue, $tokenLastUsed);
$this->tokenProvider->updateToken($series, $tokenValue, $tokenLastUsed);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public function provideCreateLoginLinkData()
];

yield [
new TestLoginLinkHandlerUser('weaverryan', 'ryan@symfonycasts.com', 'pwhash', new \DateTime('2020-06-01 00:00:00', new \DateTimeZone('+0000'))),
new TestLoginLinkHandlerUser('weaverryan', 'ryan@symfonycasts.com', 'pwhash', new \DateTimeImmutable('2020-06-01 00:00:00', new \DateTimeZone('+0000'))),
['lastAuthenticatedAt' => '2020-06-01T00:00:00+00:00'],
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,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 @@ -108,7 +108,7 @@ public function testConsumeRememberMeCookieValidByValidatorWithoutUpdate()
$verifier = $this->createMock(TokenVerifierInterface::class);
$handler = new PersistentRememberMeHandler($this->tokenProvider, 'secret', $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 @@ -135,7 +135,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 @@ -150,7 +150,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 Down
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