[PasswordHasher] Improved BC layer

This commit is contained in:
Alexander M. Turek 2021-05-01 20:09:29 +02:00
parent 2ba1f89a3a
commit 0caad4f72d
8 changed files with 229 additions and 6 deletions

View File

@ -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<string, PasswordHasherInterface|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;

View File

@ -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

View File

@ -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": [

View File

@ -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,8 +62,14 @@ class EncoderFactory implements EncoderFactoryInterface
}
if (!$this->encoders[$encoderKey] instanceof PasswordEncoderInterface) {
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];
}

View File

@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <me@derrabus.de>
*
* @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);
}
}

View File

@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <me@derrabus.de>
*
* @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);
}
}

View File

@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <me@derrabus.de>
*
* @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);
}
}

View File

@ -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';