8000 [PasswordHasher] Improved BC layer · symfony/symfony@7a3aae4 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7a3aae4

Browse files
committed
[PasswordHasher] Improved BC layer
1 parent b6966d9 commit 7a3aae4

File tree

8 files changed

+229
-6
lines changed

8 files changed

+229
-6
lines changed

src/Symfony/Component/PasswordHasher/Hasher/PasswordHasherFactory.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use Symfony\Component\PasswordHasher\Exception\LogicException;
1515
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
1616
use Symfony\Component\Security\Core\Encoder\EncoderAwareInterface;
17+
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
18+
use Symfony\Component\Security\Core\Encoder\PasswordHasherAdapter;
1719

1820
/**
1921
* A generic hasher factory implementation.
@@ -25,6 +27,9 @@ class PasswordHasherFactory implements PasswordHasherFactoryInterface
2527
{
2628
private $passwordHashers;
2729

30+
/**
31+
* @param array<string, PasswordHasherInterface|array> $passwordHashers
32+
*/
2833
public function __construct(array $passwordHashers)
2934
{
3035
$this->passwordHashers = $passwordHashers;
@@ -57,7 +62,10 @@ public function getPasswordHasher($user): PasswordHasherInterface
5762
}
5863

5964
if (!$this->passwordHashers[$hasherKey] instanceof PasswordHasherInterface) {
60-
$this->passwordHashers[$hasherKey] = $this->createHasher($this->passwordHashers[$hasherKey]);
65+
$this->passwordHashers[$hasherKey] = $this->passwordHashers[$hasherKey] instanceof PasswordEncoderInterface
66+
? new PasswordHasherAdapter($this->passwordHashers[$hasherKey])
67+
: $this->createHasher($this->passwordHashers[$hasherKey])
68+
;
6169
}
6270

6371
return $this->passwordHashers[$hasherKey];
@@ -82,6 +90,9 @@ private function createHasher(array $config, bool $isExtra = false): PasswordHas
8290
}
8391

8492
$hasher = new $config['class'](...$config['arguments']);
93+
if (!$hasher instanceof PasswordHasherInterface && $hasher instanceof PasswordEncoderInterface) {
94+
$hasher = new PasswordHasherAdapter($hasher);
95+
}
8596

8697
if ($isExtra || !\in_array($config['class'], [NativePasswordHasher::class, SodiumPasswordHasher::class], true)) {
8798
return $hasher;

src/Symfony/Component/PasswordHasher/Tests/Hasher/PasswordHasherFactoryTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface;
1919
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
2020
use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher;
21+
use Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder;
2122
use Symfony\Component\Security\Core\User\InMemoryUser;
2223
use Symfony\Component\Security\Core\User\UserInterface;
2324

@@ -176,6 +177,24 @@ public function testDefaultMigratingHashers()
176177
(new PasswordHasherFactory([SomeUser::class => ['class' => SodiumPasswordHasher::class, 'arguments' => []]]))->getPasswordHasher(SomeUser::class)
177178
);
178179
}
180+
181+
/**
182+
* @group legacy
183+
*/
184+
public function testLegacyEncoderObject()
185+
{
186+
$factory = new PasswordHasherFactory([SomeUser::class => new PlaintextPasswordEncoder()]);
187+
self::assertSame('foo{bar}', $factory->getPasswordHasher(SomeUser::class)->hash('foo', 'bar'));
188+
}
189+
190+
/**
191+
* @group legacy
192+
*/
193+
public function testLegacyEncoderClass()
194+
{
195+
$factory = new PasswordHasherFactory([SomeUser::class => ['class' => PlaintextPasswordEncoder::class, 'arguments' => []]]);
196+
self::assertSame('foo{bar}', $factory->getPasswordHasher(SomeUser::class)->hash('foo', 'bar'));
197+
}
179198
}
180199

181200
class SomeUser implements UserInterface

src/Symfony/Component/PasswordHasher/composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
"symfony/security-core": "^5.3",
2424
"symfony/console": "^5"
2525
},
26+
"conflict": {
27+
"symfony/security-core": "<5.3"
28+
},
2629
"autoload": {
2730
"psr-4": { "Symfony\\Component\\PasswordHasher\\": "" },
2831
"exclude-from-classmap": [

src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface;
1515
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
16+
use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
17+
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
1618
use Symfony\Component\Security\Core\Exception\LogicException;
1719

1820
trigger_deprecation('symfony/security-core', '5.3', 'The "%s" class is deprecated, use "%s" instead.', EncoderFactory::class, PasswordHasherFactory::class);
@@ -60,7 +62,13 @@ public function getEncoder($user)
6062
}
6163

6264
if (!$this->encoders[$encoderKey] instanceof PasswordEncoderInterface) {
63-
$this->encoders[$encoderKey] = $this->createEncoder($this->encoders[$encoderKey]);
65+
if ($this->encoders[$encoderKey] instanceof LegacyPasswordHasherInterface) {
66+
$this->encoders[$encoderKey] = new LegacyPasswordHasherEncoder($this->encoders[$encoderKey]);
67+
} elseif ($this->encoders[$encoderKey] instanceof PasswordHasherInterface) {
68+
$this->encoders[$encoderKey] = new PasswordHasherEncoder($this->encoders[$encoderKey]);
69+
} else {
70+
$this->encoders[$encoderKey] = $this->createEncoder($this->encoders[$encoderKey]);
71+
}
6472
}
6573

6674
return $this->encoders[$encoderKey];
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Encoder;
13+
14+
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
15+
use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
16+
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
17+
18+
/**
19+
* Forward compatibility for new new PasswordHasher component.
20+
*
21+
* @author Alexander M. Turek <me@derrabus.de>
22+
*
23+
* @internal To be removed in Symfony 6
24+
*/
25+
final class LegacyPasswordHasherEncoder implements PasswordEncoderInterface
26+
{
27+
private $passwordHasher;
28+
29+
public function __construct(LegacyPasswordHasherInterface $passwordHasher)
30+
{
31+
$this->passwordHasher = $passwordHasher;
32+
}
33+
34+
public function encodePassword(string $raw, ?string $salt): string
35+
{
36+
try {
37+
return $this->passwordHasher->hash($raw, $salt);
38+
} catch (InvalidPasswordException $e) {
39+
throw new BadCredentialsException($e->getMessage(), $e->getCode(), $e);
40+
}
41+
}
42+
43+
public function isPasswordValid(string $encoded, string $raw, ?string $salt): bool
44+
{
45+
return $this->passwordHasher->verify($encoded, $raw, $salt);
46+
}
47+
48+
public function needsRehash(string $encoded): bool
49+
{
50+
return $this->passwordHasher->needsRehash($encoded);
51+
}
52+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Encoder;
13+
14+
use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
15+
16+
/**
17+
* Forward compatibility for new new PasswordHasher component.
18+
*
19+
* @author Alexander M. Turek <me@derrabus.de>
20+
*
21+
* @internal To be removed in Symfony 6
22+
*/
23+
final class PasswordHasherAdapter implements LegacyPasswordHasherInterface
24+
{
25+
private $passwordEncoder;
26+
27+
public function __construct(PasswordEncoderInterface $passwordEncoder)
28+
{
29+
$this->passwordEncoder = $passwordEncoder;
30+
}
31+
32+
public function hash(string $plainPassword, ?string $salt = null): string
33+
{
34+
return $this->passwordEncoder->encodePassword($plainPassword, $salt);
35+
}
36+
37+
public function verify(string $hashedPassword, string $plainPassword, ?string $salt = null): bool
38+
{
39+
return $this->passwordEncoder->isPasswordValid($hashedPassword, $plainPassword, $salt);
40+
}
41+
42+
public function needsRehash(string $hashedPassword): bool
43+
{
44+
return $this->passwordEncoder->needsRehash($hashedPassword);
45+
}
46+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Encoder;
13+
14+
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
15+
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
16+
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
17+
18+
/**
19+
* Forward compatibility for new new PasswordHasher component.
20+
*
21+
* @author Alexander M. Turek <me@derrabus.de>
22+
*
23+
* @internal To be removed in Symfony 6
24+
*/
25+
final class PasswordHasherEncoder implements PasswordEncoderInterface, SelfSaltingEncoderInterface
26+
{
27+
private $passwordHasher;
28+
29+
public function __construct(PasswordHasherInterface $passwordHasher)
30+
{
31+
$this->passwordHasher = $passwordHasher;
32+
}
33+
34+
public function encodePassword(string $raw, ?string $salt): string
35+
{
36+
if (null !== $salt) {
37+
throw new \InvalidArgumentException('This password hasher does not support passing a salt.');
38+
}
39+
40+
try {
41+
return $this->passwordHasher->hash($raw);
42+
} catch (InvalidPasswordException $e) {
43+
throw new BadCredentialsException($e->getMessage(), $e->getCode(), $e);
44+
}
45+
}
46+
47+
public function isPasswordValid(string $encoded, string $raw, ?string $salt): bool
48+
{
49+
if (null !== $salt) {
50+
throw new \InvalidArgumentException('This password hasher does not support passing a salt.');
51+
}
52+
53+
return $this->passwordHasher->verify($encoded, $raw);
54+
}
55+
56+
public function needsRehash(string $encoded): bool
57+
{
58+
return $this->passwordHasher->needsRehash($encoded);
59+
}
60+
}

src/Symfony/Component/Security/Core/Tests/Encoder/EncoderFactoryTest.php

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,20 @@
1212
namespace Symfony\Component\Security\Core\Tests\Encoder;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\PasswordHasher\Hasher\MessageDigestPasswordHasher;
16+
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
17+
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface;
18+
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
19+
use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher;
1520
use Symfony\Component\Security\Core\Encoder\EncoderAwareInterface;
1621
use Symfony\Component\Security\Core\Encoder\EncoderFactory;
1722
use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder;
1823
use Symfony\Component\Security\Core\Encoder\MigratingPasswordEncoder;
1924
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
25+
use Symfony\Component\Security\Core\Encoder\SelfSaltingEncoderInterface;
2026
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
2127
use Symfony\Component\Security\Core\User\User;
2228
use Symfony\Component\Security\Core\User\UserInterface;
23-
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface;
24-
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
25-
use Symfony\Component\PasswordHasher\Hasher\MessageDigestPasswordHasher;
2629

2730
/**
2831
* @group legacy
@@ -193,6 +196,28 @@ public function testHasherAwareCompat()
193196
$expectedEncoder = new MessageDigestPasswordHasher('sha1');
194197
$this->assertEquals($expectedEncoder->hash('foo', ''), $encoder->hash('foo', ''));
195198
}
199+
200+
public function testLegacyPasswordHasher()
201+
{
202+
$factory = new EncoderFactory([
203+
SomeUser::class => new PlaintextPasswordHasher(),
204+
]);
205+
206+
$encoder = $factory->getEncoder(new SomeUser());
207+
self::assertNotInstanceOf(SelfSaltingEncoderInterface::class, $encoder);
208+
self::assertSame('foo{bar}', $encoder->encodePassword('foo', 'bar'));
209+
}
210+
211+
public function testPasswordHasher()
212+
{
213+
$factory = new EncoderFactory([
214+
SomeUser::class => new NativePasswordHasher(),
215+
]);
216+
217+
$encoder = $factory->getEncoder(new SomeUser());
218+
self::assertInstanceOf(SelfSaltingEncoderInterface::class, $encoder);
219+
self::assertTrue($encoder->isPasswordValid($encoder->encodePassword('foo', null), 'foo', null));
220+
}
196221
}
197222

198223
class SomeUser implements UserInterface
@@ -236,7 +261,6 @@ public function getEncoderName(): ?string
236261
}
237262
}
238263

239-
240264
class HasherAwareUser extends SomeUser implements PasswordHasherAwareInterface
241265
{
242266
public $hasherName = 'encoder_name';

0 commit comments

Comments
 (0)
0