Lines
94.73%
18 / 19
Methods
66.66%
2 / 3
Classes
0.00%
0 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| issue | 88.88% | 8 / 9 | 0.00% | 0 / 1 | 3.01 | ||
| verify | 100.00% | 9 / 9 | 100.00% | 1 / 1 | 7 | ||
| sign | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| 27 | final class PasswordResetTokenService | |
| 28 | { | |
| 29 | public const int DEFAULT_LIFETIME_SECONDS = 3600; | |
| 30 | ||
| 31 | public const int MAX_LIFETIME_SECONDS = 86_400; | |
| 32 | ||
| 33 | /** | |
| 34 | * @return array{userId: int, expires: int, signature: string} | |
| 35 | */ | |
| 36 | public function issue(int $userId, string $passwordKey, ?int $lifetimeSeconds = null): array | |
| 37 | { | |
| 38 | $lifetime = $lifetimeSeconds ?? self::DEFAULT_LIFETIME_SECONDS; | |
| 39 | if ($lifetime < 60 || $lifetime > self::MAX_LIFETIME_SECONDS) { | |
| 40 | $lifetime = self::DEFAULT_LIFETIME_SECONDS; | |
| 41 | } | |
| 42 | ||
| 43 | $expires = time() + $lifetime; | |
| 44 | ||
| 45 | return [ | |
| 46 | 'userId' => $userId, | |
| 47 | 'expires' => $expires, | |
| 48 | 'signature' => $this->sign($userId, $expires, $passwordKey), | |
| 49 | ]; | |
| 50 | } | |
| 51 | ||
| 52 | public function verify(int $userId, int $expires, string $signature, string $passwordKey): bool | |
| 53 | { | |
| 54 | if ($userId <= 0 || $expires <= 0 || $signature === '' || $passwordKey === '') { | |
| 55 | return false; | |
| 56 | } | |
| 57 | ||
| 58 | $now = time(); | |
| 59 | if ($expires < $now) { | |
| 60 | return false; | |
| 61 | } | |
| 62 | ||
| 63 | if ($expires > ($now + self::MAX_LIFETIME_SECONDS)) { | |
| 64 | return false; | |
| 65 | } | |
| 66 | ||
| 67 | $expected = $this->sign($userId, $expires, $passwordKey); | |
| 68 | ||
| 69 | return hash_equals($expected, $signature); | |
| 70 | } | |
| 71 | ||
| 72 | private function sign(int $userId, int $expires, string $passwordKey): string | |
| 73 | { | |
| 74 | return hash_hmac('sha256', $userId . '|' . $expires, $passwordKey); | |
| 75 | } | |
| 76 | } |