reset-password-bundle: Incorrect expiration time rendering with PHP 8.1 bug

Expiration time is once again not being rendered correctly. A new Symfony 6 project with make:reset-password displays the following:

If an account matching your email exists, then an email was just sent that contains a link that you can use to reset your password. This link will expire in 11 months.

If you don’t receive an email please check your spam folder or try again.

The reset token lifetime is 3600 by default, and the behavior is the same if set explicitly.

This issue appears to have been reported in the past (#146), but I’m opening a new ticket since the code has been updated to use translations.

The problem appears to arise from a bad interval that results from time zone confusion (this is probably a bug in PHP). I have distilled the problem to the following snippet, which is what ResetPasswordHelper:71 and ResetPasswordToken:141 are doing under the hood:

// creates new DateTime for the current timezone (UTC-5 in my case)
$expiresAt = new \DateTimeImmutable('+3600 seconds'); 
echo 'expires: ' . $expiresAt->format(\DateTimeInterface::ISO8601) . "\n";

$generatedAt = ($expiresAt->getTimestamp() - 3600);

// creates new DateTime in UTC
$createdAtTime = \DateTimeImmutable::createFromFormat('U', (string) $generatedAt);
echo 'created: ' . $createdAtTime->format(\DateTimeInterface::ISO8601) . "\n";

$interval = $expiresAt->diff($createdAtTime);
echo 'computed interval: ' . $interval->format('%R[%yy %mM %dd %hh %im %ss]') . "\n";

This snippet outputs, for example

expires: 2022-03-21T19:07:42-0500
created: 2022-03-21T23:07:42+0000
computed interval: -[-1y 11M 31d 0h 0m 0s]

Setting the $expiresAt timezone to UTC produces the correct result:

expires: 2022-03-22T00:11:56+0000
created: 2022-03-21T23:11:56+0000
computed interval: -[0y 0M 0d 1h 0m 0s]

Hence, one possible solution would be to enforce a UTC timezone for all dates. There was a similar workaround when the expiration interval was rendered in templates using the date Twig filter. (e.g., expiresInterval|date('g', 'UTC')). However, an analogous template-driven workaround is no longer accessible since the label generation happens within the ResetPasswordToken class itself when generating the translation message key and data.

My advice is to change the expiration DateTime generation at ResetPasswordHelper:71 to the following:

$expiresAt = new \DateTimeImmutable(sprintf('+%d seconds', $this->resetRequestLifetime), new \DateTimeZone('UTC'));

I can submit a pull request if you would like.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 2
  • Comments: 20

Commits related to this issue

Most upvoted comments

A possible solution, regardless of the mentioned PHP bug fix, is to change the date diff calculations from Unix timestamp to a DateTime(Immutable).

I have applied the following patch to my code, which proved to be a solution. In both PHP versions (7.4.30 and 8.1.7) the expiration time is shown as “30 minutes” (I had the lifetime set to 1800 seconds). Without the patch, on PHP 8.1.7, 11 months was shown as expiration time.

I can submit a pull request if you would like.

diff --git a/src/Model/ResetPasswordToken.php b/src/Model/ResetPasswordToken.php
index 46f6767..8d60d0c 100644
--- a/src/Model/ResetPasswordToken.php
+++ b/src/Model/ResetPasswordToken.php
@@ -26,7 +26,7 @@ final class ResetPasswordToken
     private $expiresAt;
 
     /**
-     * @var int|null timestamp when the token was created
+     * @var \DateTimeImmutable|null timestamp when the token was created
      */
     private $generatedAt;
 
@@ -35,7 +35,7 @@ final class ResetPasswordToken
      */
     private $transInterval = 0;
 
-    public function __construct(string $token, \DateTimeInterface $expiresAt, int $generatedAt = null)
+    public function __construct(string $token, \DateTimeInterface $expiresAt, \DateTimeImmutable $generatedAt = null)
     {
         $this->token = $token;
         $this->expiresAt = $expiresAt;
@@ -138,9 +138,7 @@ final class ResetPasswordToken
             throw new \LogicException(sprintf('%s initialized without setting the $generatedAt timestamp.', self::class));
         }
 
-        $createdAtTime = \DateTimeImmutable::createFromFormat('U', (string) $this->generatedAt);
-
-        return $this->expiresAt->diff($createdAtTime);
+        return $this->expiresAt->diff($this->generatedAt);
     }
 
     private function triggerDeprecation(): void
diff --git a/src/ResetPasswordHelper.php b/src/ResetPasswordHelper.php
index a6b2b6a..bda69e5 100644
--- a/src/ResetPasswordHelper.php
+++ b/src/ResetPasswordHelper.php
@@ -68,9 +68,8 @@ class ResetPasswordHelper implements ResetPasswordHelperInterface
             throw new TooManyPasswordRequestsException($availableAt);
         }
 
-        $expiresAt = new \DateTimeImmutable(sprintf('+%d seconds', $this->resetRequestLifetime));
-
-        $generatedAt = ($expiresAt->getTimestamp() - $this->resetRequestLifetime);
+        $generatedAt = new \DateTimeImmutable();
+        $expiresAt = $generatedAt->modify(sprintf('+%d seconds', $this->resetRequestLifetime));
 
         $tokenComponents = $this->tokenGenerator->createToken($expiresAt, $this->repository->getUserIdentifier($user));

Can we maybe get a patch here depending on the php version until this is fixed?

As php version don’t get updated nearly as fast as deps and to avoid having to mark the whole php 8.1 as a conflicting dep of this bundle (which in turn would make it symfony 6.1 incompatible)

To follow up, I think I found the root of the problem in the PHP source code and added a comment to the PHP bug report linked above.

Just tested it with 8.1.10, works again 🥳.

@Raghav9888 you are spamming the thread of a closed issue with something that has nothing to do with it.

You seem to be editing files in the /vendor directory. Which you should never do

Delete the vendor folder and rerun composer install

If the issue is still there Google the error message. Open an issue in the relevant github repository if nothing turns up

@Raghav9888 Update your system’s php version or if you don’t care about the bug, install an older version of the bundle

Wow I was wondering why it was saying the token expires in 0 minutes and scratching my head for why the config option wasn’t applying guess that’s why…