diff --git a/CHANGELOG.md b/CHANGELOG.md index aa6192a..b616dc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 1.1.1 under development -- no changes in this release. +- New #64: Add `PasswordHasher::needsRehash()` method (@Gerych1984) ## 1.1.0 February 26, 2025 diff --git a/src/PasswordHasher.php b/src/PasswordHasher.php index a024a6a..7702604 100644 --- a/src/PasswordHasher.php +++ b/src/PasswordHasher.php @@ -4,8 +4,16 @@ namespace Yiisoft\Security; +use InvalidArgumentException; use SensitiveParameter; +use function password_hash; +use function password_needs_rehash; +use function password_verify; + +use const PASSWORD_BCRYPT; +use const PASSWORD_DEFAULT; + /** * PasswordHasher allows generating password hash and verifying passwords against a hash. */ @@ -27,8 +35,8 @@ final class PasswordHasher * @see https://www.php.net/manual/en/function.password-hash.php */ public function __construct( - private readonly ?string $algorithm = PASSWORD_DEFAULT, - ?array $parameters = null, + private readonly string|null $algorithm = PASSWORD_DEFAULT, + array|null $parameters = null, ) { if ($parameters === null) { $this->parameters = self::SAFE_PARAMETERS[$this->algorithm] ?? []; @@ -79,7 +87,7 @@ public function hash( * @param string $password The password to verify. * @param string $hash The hash to verify the password against. * - * @throws \InvalidArgumentException on bad password/hash parameters or if crypt() with Blowfish hash is not + * @throws InvalidArgumentException on bad password/hash parameters or if crypt() with Blowfish hash is not * available. * * @return bool whether the password is correct. @@ -93,9 +101,25 @@ public function validate( string $hash ): bool { if ($password === '') { - throw new \InvalidArgumentException('Password must be a string and cannot be empty.'); + throw new InvalidArgumentException('Password must be a string and cannot be empty.'); } return password_verify($password, $hash); } + + /** + * Verifies if a hash needs rehash. + * + * @param string $hash The hash to verify. + * + * @return bool Whether rehash is needed. + * + * @see https://www.php.net/manual/function.password-needs-rehash.php + */ + public function needsRehash( + #[SensitiveParameter] + string $hash + ): bool { + return password_needs_rehash($hash, $this->algorithm, $this->parameters); + } } diff --git a/tests/PasswordHasherTest.php b/tests/PasswordHasherTest.php index d29e4db..f3b60ce 100644 --- a/tests/PasswordHasherTest.php +++ b/tests/PasswordHasherTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\Security\Tests; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Yiisoft\Security\PasswordHasher; @@ -38,7 +39,7 @@ public function testPasswordHash(): void public function testValidateEmptyPasswordException(): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $password = new PasswordHasher(); $password->validate('', 'test'); @@ -58,4 +59,20 @@ public function testPreconfiguredAlgorithm(): void $hasher = new PasswordHasher(PASSWORD_BCRYPT); $this->assertSame('$2y$13$', substr($hasher->hash('42'), 0, 7)); } + + public function testNeedsRehashTrue(): void + { + $hash = (new PasswordHasher(PASSWORD_BCRYPT, ['cost' => 12]))->hash('test'); + $hasher = new PasswordHasher(PASSWORD_BCRYPT, ['cost' => 13]); + + $this->assertTrue($hasher->needsRehash($hash)); + } + + public function testNeedsRehashFalse(): void + { + $hasher = new PasswordHasher(); + $hash = $hasher->hash('test'); + + $this->assertFalse($hasher->needsRehash($hash)); + } }