89 lines
2.1 KiB
PHP

<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Security\PasswordRecover;
use Chill\MainBundle\Entity\User;
use Psr\Log\LoggerInterface;
class TokenManager
{
final public const HASH = 'h';
final public const TIMESTAMP = 'ts';
final public const TOKEN = 't';
final public const TOKEN_LENGTH = 24;
final public const USERNAME_CANONICAL = 'u';
/**
* @var LoggerInterface
*/
protected $logger;
/**
* @param string $secret
*/
public function __construct(protected $secret, LoggerInterface $logger)
{
$this->logger = $logger;
}
public function generate(User $user, \DateTimeInterface $expiration)
{
$token = \random_bytes(self::TOKEN_LENGTH);
$username = $user->getUsernameCanonical();
if (empty($username)) {
throw new \UnexpectedValueException('username should not be empty to generate a token');
}
$timestamp = (string) $expiration->getTimestamp();
$hash = \hash('sha1', $token.$username.$timestamp.$this->secret);
return [
self::HASH => $hash,
self::TOKEN => \bin2hex($token),
self::TIMESTAMP => $timestamp,
self::USERNAME_CANONICAL => $username,
];
}
public function verify($hash, $token, User $user, string $timestamp)
{
$token = \hex2bin(\trim((string) $token));
if (self::TOKEN_LENGTH !== \strlen($token)) {
return false;
}
$username = $user->getUsernameCanonical();
$date = \DateTimeImmutable::createFromFormat('U', $timestamp);
if ($date < new \DateTime('now')) {
$this->logger->info('receiving a password recover token with expired '
.'validity');
return false;
}
$expected = \hash('sha1', $token.$username.$timestamp.$this->secret);
if ($expected !== $hash) {
return false;
}
return true;
}
}