diff --git a/src/Symfony/Component/PasswordHasher/Hasher/PasswordHasherFactory.php b/src/Symfony/Component/PasswordHasher/Hasher/PasswordHasherFactory.php index b75573f746..62c5e37e85 100644 --- a/src/Symfony/Component/PasswordHasher/Hasher/PasswordHasherFactory.php +++ b/src/Symfony/Component/PasswordHasher/Hasher/PasswordHasherFactory.php @@ -14,6 +14,8 @@ namespace Symfony\Component\PasswordHasher\Hasher; use Symfony\Component\PasswordHasher\Exception\LogicException; use Symfony\Component\PasswordHasher\PasswordHasherInterface; use Symfony\Component\Security\Core\Encoder\EncoderAwareInterface; +use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; +use Symfony\Component\Security\Core\Encoder\PasswordHasherAdapter; /** * A generic hasher factory implementation. @@ -25,6 +27,9 @@ class PasswordHasherFactory implements PasswordHasherFactoryInterface { private $passwordHashers; + /** + * @param array $passwordHashers + */ public function __construct(array $passwordHashers) { $this->passwordHashers = $passwordHashers; @@ -57,7 +62,10 @@ class PasswordHasherFactory implements PasswordHasherFactoryInterface } if (!$this->passwordHashers[$hasherKey] instanceof PasswordHasherInterface) { - $this->passwordHashers[$hasherKey] = $this->createHasher($this->passwordHashers[$hasherKey]); + $this->passwordHashers[$hasherKey] = $this->passwordHashers[$hasherKey] instanceof PasswordEncoderInterface + ? new PasswordHasherAdapter($this->passwordHashers[$hasherKey]) + : $this->createHasher($this->passwordHashers[$hasherKey]) + ; } return $this->passwordHashers[$hasherKey]; @@ -82,6 +90,9 @@ class PasswordHasherFactory implements PasswordHasherFactoryInterface } $hasher = new $config['class'](...$config['arguments']); + if (!$hasher instanceof PasswordHasherInterface && $hasher instanceof PasswordEncoderInterface) { + $hasher = new PasswordHasherAdapter($hasher); + } if ($isExtra || !\in_array($config['class'], [NativePasswordHasher::class, SodiumPasswordHasher::class], true)) { return $hasher; diff --git a/src/Symfony/Component/PasswordHasher/Tests/Hasher/PasswordHasherFactoryTest.php b/src/Symfony/Component/PasswordHasher/Tests/Hasher/PasswordHasherFactoryTest.php index 633e38e39e..bebd95b195 100644 --- a/src/Symfony/Component/PasswordHasher/Tests/Hasher/PasswordHasherFactoryTest.php +++ b/src/Symfony/Component/PasswordHasher/Tests/Hasher/PasswordHasherFactoryTest.php @@ -18,6 +18,7 @@ use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher; use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface; use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory; use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher; +use Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder; use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\UserInterface; @@ -176,6 +177,24 @@ class PasswordHasherFactoryTest extends TestCase (new PasswordHasherFactory([SomeUser::class => ['class' => SodiumPasswordHasher::class, 'arguments' => []]]))->getPasswordHasher(SomeUser::class) ); } + + /** + * @group legacy + */ + public function testLegacyEncoderObject() + { + $factory = new PasswordHasherFactory([SomeUser::class => new PlaintextPasswordEncoder()]); + self::assertSame('foo{bar}', $factory->getPasswordHasher(SomeUser::class)->hash('foo', 'bar')); + } + + /** + * @group legacy + */ + public function testLegacyEncoderClass() + { + $factory = new PasswordHasherFactory([SomeUser::class => ['class' => PlaintextPasswordEncoder::class, 'arguments' => []]]); + self::assertSame('foo{bar}', $factory->getPasswordHasher(SomeUser::class)->hash('foo', 'bar')); + } } class SomeUser implements UserInterface diff --git a/src/Symfony/Component/PasswordHasher/composer.json b/src/Symfony/Component/PasswordHasher/composer.json index 2ed22ee706..0098950760 100644 --- a/src/Symfony/Component/PasswordHasher/composer.json +++ b/src/Symfony/Component/PasswordHasher/composer.json @@ -23,6 +23,9 @@ "symfony/security-core": "^5.3", "symfony/console": "^5" }, + "conflict": { + "symfony/security-core": "<5.3" + }, "autoload": { "psr-4": { "Symfony\\Component\\PasswordHasher\\": "" }, "exclude-from-classmap": [ diff --git a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php index d1855aa1e4..526c461e5e 100644 --- a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php +++ b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php @@ -13,6 +13,8 @@ namespace Symfony\Component\Security\Core\Encoder; use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface; use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory; +use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface; +use Symfony\Component\PasswordHasher\PasswordHasherInterface; use Symfony\Component\Security\Core\Exception\LogicException; trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', EncoderFactory::class, PasswordHasherFactory::class); @@ -60,7 +62,13 @@ class EncoderFactory implements EncoderFactoryInterface } if (!$this->encoders[$encoderKey] instanceof PasswordEncoderInterface) { - $this->encoders[$encoderKey] = $this->createEncoder($this->encoders[$encoderKey]); + if ($this->encoders[$encoderKey] instanceof LegacyPasswordHasherInterface) { + $this->encoders[$encoderKey] = new LegacyPasswordHasherEncoder($this->encoders[$encoderKey]); + } elseif ($this->encoders[$encoderKey] instanceof PasswordHasherInterface) { + $this->encoders[$encoderKey] = new PasswordHasherEncoder($this->encoders[$encoderKey]); + } else { + $this->encoders[$encoderKey] = $this->createEncoder($this->encoders[$encoderKey]); + } } return $this->encoders[$encoderKey]; diff --git a/src/Symfony/Component/Security/Core/Encoder/LegacyPasswordHasherEncoder.php b/src/Symfony/Component/Security/Core/Encoder/LegacyPasswordHasherEncoder.php new file mode 100644 index 0000000000..7e57ff2383 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Encoder/LegacyPasswordHasherEncoder.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Encoder; + +use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException; +use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; + +/** + * Forward compatibility for new new PasswordHasher component. + * + * @author Alexander M. Turek + * + * @internal To be removed in Symfony 6 + */ +final class LegacyPasswordHasherEncoder implements PasswordEncoderInterface +{ + private $passwordHasher; + + public function __construct(LegacyPasswordHasherInterface $passwordHasher) + { + $this->passwordHasher = $passwordHasher; + } + + public function encodePassword(string $raw, ?string $salt): string + { + try { + return $this->passwordHasher->hash($raw, $salt); + } catch (InvalidPasswordException $e) { + throw new BadCredentialsException($e->getMessage(), $e->getCode(), $e); + } + } + + public function isPasswordValid(string $encoded, string $raw, ?string $salt): bool + { + return $this->passwordHasher->verify($encoded, $raw, $salt); + } + + public function needsRehash(string $encoded): bool + { + return $this->passwordHasher->needsRehash($encoded); + } +} diff --git a/src/Symfony/Component/Security/Core/Encoder/PasswordHasherAdapter.php b/src/Symfony/Component/Security/Core/Encoder/PasswordHasherAdapter.php new file mode 100644 index 0000000000..4a4b9c0b13 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Encoder/PasswordHasherAdapter.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Encoder; + +use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface; + +/** + * Forward compatibility for new new PasswordHasher component. + * + * @author Alexander M. Turek + * + * @internal To be removed in Symfony 6 + */ +final class PasswordHasherAdapter implements LegacyPasswordHasherInterface +{ + private $passwordEncoder; + + public function __construct(PasswordEncoderInterface $passwordEncoder) + { + $this->passwordEncoder = $passwordEncoder; + } + + public function hash(string $plainPassword, ?string $salt = null): string + { + return $this->passwordEncoder->encodePassword($plainPassword, $salt); + } + + public function verify(string $hashedPassword, string $plainPassword, ?string $salt = null): bool + { + return $this->passwordEncoder->isPasswordValid($hashedPassword, $plainPassword, $salt); + } + + public function needsRehash(string $hashedPassword): bool + { + return $this->passwordEncoder->needsRehash($hashedPassword); + } +} diff --git a/src/Symfony/Component/Security/Core/Encoder/PasswordHasherEncoder.php b/src/Symfony/Component/Security/Core/Encoder/PasswordHasherEncoder.php new file mode 100644 index 0000000000..d37875dcc2 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Encoder/PasswordHasherEncoder.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Encoder; + +use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException; +use Symfony\Component\PasswordHasher\PasswordHasherInterface; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; + +/** + * Forward compatibility for new new PasswordHasher component. + * + * @author Alexander M. Turek + * + * @internal To be removed in Symfony 6 + */ +final class PasswordHasherEncoder implements PasswordEncoderInterface, SelfSaltingEncoderInterface +{ + private $passwordHasher; + + public function __construct(PasswordHasherInterface $passwordHasher) + { + $this->passwordHasher = $passwordHasher; + } + + public function encodePassword(string $raw, ?string $salt): string + { + if (null !== $salt) { + throw new \InvalidArgumentException('This password hasher does not support passing a salt.'); + } + + try { + return $this->passwordHasher->hash($raw); + } catch (InvalidPasswordException $e) { + throw new BadCredentialsException($e->getMessage(), $e->getCode(), $e); + } + } + + public function isPasswordValid(string $encoded, string $raw, ?string $salt): bool + { + if (null !== $salt) { + throw new \InvalidArgumentException('This password hasher does not support passing a salt.'); + } + + return $this->passwordHasher->verify($encoded, $raw); + } + + public function needsRehash(string $encoded): bool + { + return $this->passwordHasher->needsRehash($encoded); + } +} diff --git a/src/Symfony/Component/Security/Core/Tests/Encoder/EncoderFactoryTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/EncoderFactoryTest.php index 3744e05bc0..7b05c9be0a 100644 --- a/src/Symfony/Component/Security/Core/Tests/Encoder/EncoderFactoryTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/EncoderFactoryTest.php @@ -12,17 +12,20 @@ namespace Symfony\Component\Security\Core\Tests\Encoder; use PHPUnit\Framework\TestCase; +use Symfony\Component\PasswordHasher\Hasher\MessageDigestPasswordHasher; +use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory; +use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher; use Symfony\Component\Security\Core\Encoder\EncoderAwareInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactory; use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder; use Symfony\Component\Security\Core\Encoder\MigratingPasswordEncoder; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; +use Symfony\Component\Security\Core\Encoder\SelfSaltingEncoderInterface; use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface; -use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory; -use Symfony\Component\PasswordHasher\Hasher\MessageDigestPasswordHasher; /** * @group legacy @@ -193,6 +196,28 @@ class EncoderFactoryTest extends TestCase $expectedEncoder = new MessageDigestPasswordHasher('sha1'); $this->assertEquals($expectedEncoder->hash('foo', ''), $encoder->hash('foo', '')); } + + public function testLegacyPasswordHasher() + { + $factory = new EncoderFactory([ + SomeUser::class => new PlaintextPasswordHasher(), + ]); + + $encoder = $factory->getEncoder(new SomeUser()); + self::assertNotInstanceOf(SelfSaltingEncoderInterface::class, $encoder); + self::assertSame('foo{bar}', $encoder->encodePassword('foo', 'bar')); + } + + public function testPasswordHasher() + { + $factory = new EncoderFactory([ + SomeUser::class => new NativePasswordHasher(), + ]); + + $encoder = $factory->getEncoder(new SomeUser()); + self::assertInstanceOf(SelfSaltingEncoderInterface::class, $encoder); + self::assertTrue($encoder->isPasswordValid($encoder->encodePassword('foo', null), 'foo', null)); + } } class SomeUser implements UserInterface @@ -236,7 +261,6 @@ class EncAwareUser extends SomeUser implements EncoderAwareInterface } } - class HasherAwareUser extends SomeUser implements PasswordHasherAwareInterface { public $hasherName = 'encoder_name';