Skip to content

[PasswordHasher] Improved BC layer #41022

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
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.
Expand All @@ -25,6 +27,9 @@ class PasswordHasherFactory implements PasswordHasherFactoryInterface
{
private $passwordHashers;

/**
* @param array<string, PasswordHasherInterface|array> $passwordHashers
*/
public function __construct(array $passwordHashers)
{
$this->passwordHashers = $passwordHashers;
Expand Down Expand Up @@ -57,7 +62,10 @@ public function getPasswordHasher($user): PasswordHasherInterface
}

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];
Expand All @@ -82,6 +90,9 @@ private function createHasher(array $config, bool $isExtra = false): PasswordHas
}

$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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
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;

Expand Down Expand Up @@ -176,6 +177,24 @@ public function testDefaultMigratingHashers()
(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
Expand Down
3 changes: 3 additions & 0 deletions src/Symfony/Component/PasswordHasher/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
10 changes: 9 additions & 1 deletion src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

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);
Expand Down Expand Up @@ -60,7 +62,13 @@ public function getEncoder($user)
}

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];
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -193,6 +196,28 @@ public function testHasherAwareCompat()
$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
Expand Down Expand Up @@ -236,7 +261,6 @@ public function getEncoderName(): ?string
}
}


class HasherAwareUser extends SomeUser implements PasswordHasherAwareInterface
{
public $hasherName = 'encoder_name';
Expand Down