Skip to content

Commit 6726b80

Browse files
jrushlowweaverryan
authored andcommitted
improve token expiration
1 parent 6a4bd24 commit 6726b80

File tree

7 files changed

+185
-4
lines changed

7 files changed

+185
-4
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ find a change that break's semver, please create an issue.*
55

66
## NEXT
77

8+
- [#135](https://github.com/SymfonyCasts/reset-password-bundle/pull/134) Add translation support for signature expiration time
9+
- [#135](https://github.com/SymfonyCasts/reset-password-bundle/pull/134) Fixed invalid signature expiration time
10+
811
## v1.2.0
912

1013
*Dec 10th, 2020*
1114

12-
- #134 - Allow the bundle to be used with PHP 8 - thanks to @ker0x
15+
- [#134](https://github.com/SymfonyCasts/reset-password-bundle/pull/134) - Allow the bundle to be used with PHP 8 - thanks to @ker0x
1316

1417
## v1.1.0
1518

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"php": ">=7.2.5",
99
"symfony/config": "^4.4 | ^5.0",
1010
"symfony/dependency-injection": "^4.4 | ^5.0",
11+
"symfony/deprecation-contracts": "^2.2",
1112
"symfony/http-kernel": "^4.4 | ^5.0"
1213
},
1314
"require-dev": {

src/Exception/GeneratedAtNotSetException.php

Whitespace-only changes.

src/Model/ResetPasswordToken.php

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,25 @@ final class ResetPasswordToken
2525
*/
2626
private $expiresAt;
2727

28-
public function __construct(string $token, \DateTimeInterface $expiresAt)
28+
/**
29+
* @var int|null timestamp when the token was created
30+
*/
31+
private $generatedAt;
32+
33+
/**
34+
* @var int expiresAt translator interval
35+
*/
36+
private $transInterval = 0;
37+
38+
public function __construct(string $token, \DateTimeInterface $expiresAt, int $generatedAt = null)
2939
{
3040
$this->token = $token;
3141
$this->expiresAt = $expiresAt;
42+
$this->generatedAt = $generatedAt;
43+
44+
if (null === $generatedAt) {
45+
$this->triggerDeprecation();
46+
}
3247
}
3348

3449
/**
@@ -47,4 +62,82 @@ public function getExpiresAt(): \DateTimeInterface
4762
{
4863
return $this->expiresAt;
4964
}
65+
66+
/**
67+
* Get the translation message for when a token expires.
68+
*
69+
* This is used in conjunction with getExpirationMessageData() method.
70+
* Example usage in a Twig template:
71+
*
72+
* <p>{{ components.expirationMessageKey|trans(components.expirationMessageData) }}</p>
73+
*
74+
* symfony/translation is required to translate into a non-English locale.
75+
*
76+
* @throws \LogicException
77+
*/
78+
public function getExpirationMessageKey(): string
79+
{
80+
$interval = $this->getExpiresAtIntervalInstance();
81+
82+
switch ($interval) {
83+
case $interval->y > 0:
84+
$this->transInterval = $interval->y;
85+
86+
return '%count% year|%count% years';
87+
case $interval->m > 0:
88+
$this->transInterval = $interval->m;
89+
90+
return '%count% month|%count% months';
91+
case $interval->d > 0:
92+
$this->transInterval = $interval->d;
93+
94+
return '%count% day|%count% days';
95+
case $interval->h > 0:
96+
$this->transInterval = $interval->h;
97+
98+
return '%count% hour|%count% hours';
99+
default:
100+
$this->transInterval = $interval->i;
101+
102+
return '%count% minute|%count% minutes';
103+
}
104+
}
105+
106+
/**
107+
* @throws \LogicException
108+
*/
109+
public function getExpirationMessageData(): array
110+
{
111+
$this->getExpirationMessageKey();
112+
113+
return ['%count%' => $this->transInterval];
114+
}
115+
116+
/**
117+
* Get the interval that the token is valid for.
118+
*
119+
* @throws \LogicException
120+
*
121+
* @psalm-suppress PossiblyFalseArgument
122+
*/
123+
public function getExpiresAtIntervalInstance(): \DateInterval
124+
{
125+
if (null === $this->generatedAt) {
126+
throw new \LogicException(\sprintf('%s initialized without setting the $generatedAt timestamp.', self::class));
127+
}
128+
129+
$createdAtTime = \DateTimeImmutable::createFromFormat('U', (string) $this->generatedAt);
130+
131+
return $this->expiresAt->diff($createdAtTime);
132+
}
133+
134+
private function triggerDeprecation(): void
135+
{
136+
trigger_deprecation(
137+
'symfonycasts/reset-password-bundle',
138+
'1.2',
139+
'Initializing the %s without setting the "$generatedAt" constructor argument is deprecated. The default "null" will be removed in the future.',
140+
self::class
141+
);
142+
}
50143
}

src/ResetPasswordHelper.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ public function generateResetToken(object $user): ResetPasswordToken
6868
throw new TooManyPasswordRequestsException($availableAt);
6969
}
7070

71-
$expiresAt = new \DateTimeImmutable(\sprintf('+%d seconds', $this->resetRequestLifetime));
71+
$generatedAt = \time();
72+
$expiresAtTimestamp = $generatedAt + $this->resetRequestLifetime;
73+
74+
/** @var \DateTimeImmutable $expiresAt */
75+
$expiresAt = \DateTimeImmutable::createFromFormat('U', (string) $expiresAtTimestamp);
7276

7377
$tokenComponents = $this->tokenGenerator->createToken($expiresAt, $this->repository->getUserIdentifier($user));
7478

@@ -84,7 +88,8 @@ public function generateResetToken(object $user): ResetPasswordToken
8488
// final "public" token is the selector + non-hashed verifier token
8589
return new ResetPasswordToken(
8690
$tokenComponents->getPublicToken(),
87-
$expiresAt
91+
$expiresAt,
92+
$generatedAt
8893
);
8994
}
9095

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
3+
<file original="file.ext" source-language="en" datatype="plaintext">
4+
<body>
5+
<trans-unit id="1">
6+
<source>%count% year|%count% years</source>
7+
<target>%count% year|%count% years</target>
8+
</trans-unit>
9+
<trans-unit id="2">
10+
<source>%count% month|%count% months</source>
11+
<target>%count% month|%count% months</target>
12+
</trans-unit>
13+
<trans-unit id="3">
14+
<source>%count% day|%count% days</source>
15+
<target>%count% day|%count% days</target>
16+
</trans-unit>
17+
<trans-unit id="4">
18+
<source>%count% hour|%count% hours</source>
19+
<target>%count% hour|%count% hours</target>
20+
</trans-unit>
21+
<trans-unit id="5">
22+
<source>%count% minute|%count% minutes</source>
23+
<target>%count% minute|%count% minutes</target>
24+
</trans-unit>
25+
</body>
26+
</file>
27+
</xliff>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the SymfonyCasts ResetPasswordBundle package.
5+
* Copyright (c) SymfonyCasts <https://symfonycasts.com/>
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace SymfonyCasts\Bundle\ResetPassword\Tests\UnitTests\Model;
11+
12+
use PHPUnit\Framework\TestCase;
13+
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordToken;
14+
15+
/**
16+
* @author Jesse Rushlow <[email protected]>
17+
*/
18+
class ResetPasswordTokenTest extends TestCase
19+
{
20+
/**
21+
* @dataProvider translationIntervalDataProvider
22+
*/
23+
public function testTranslations(int $lifetime, int $expectedInterval, string $unitOfMeasure): void
24+
{
25+
$created = \time();
26+
27+
$expire = \DateTimeImmutable::createFromFormat('U', (string) ($created + $lifetime));
28+
29+
$token = new ResetPasswordToken('token', $expire, $created);
30+
31+
self::assertSame(
32+
\sprintf('%%count%% %s|%%count%% %ss', $unitOfMeasure, $unitOfMeasure),
33+
$token->getExpirationMessageKey()
34+
);
35+
36+
self::assertSame(['%count%' => $expectedInterval], $token->getExpirationMessageData());
37+
}
38+
39+
public function translationIntervalDataProvider(): \Generator
40+
{
41+
yield [60, 1, 'minute'];
42+
yield [900, 15, 'minute'];
43+
yield [3600, 1, 'hour'];
44+
yield [7200, 2, 'hour'];
45+
yield [43200, 12, 'hour'];
46+
yield [86400, 1, 'day'];
47+
yield [864000, 10, 'day'];
48+
yield [2678400, 1, 'month'];
49+
yield [5356800, 2, 'month'];
50+
yield [34819200, 1, 'year'];
51+
}
52+
}

0 commit comments

Comments
 (0)