Skip to content

Commit 85f0789

Browse files
[Security] add MigratingPasswordEncoder
1 parent 63d7309 commit 85f0789

File tree

4 files changed

+164
-1
lines changed

4 files changed

+164
-1
lines changed

src/Symfony/Component/Security/CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
4.4.0
5+
-----
6+
7+
* Added `MigratingPasswordEncoder`
8+
49
4.3.0
510
-----
611

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

+13-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,19 @@ private function createEncoder(array $config)
8585
private function getEncoderConfigFromAlgorithm($config)
8686
{
8787
if ('auto' === $config['algorithm']) {
88-
$config['algorithm'] = SodiumPasswordEncoder::isSupported() ? 'sodium' : 'native';
88+
$encoderChain = [];
89+
// "plaintext" is not listed as any leaked hashes could then be used to authenticate directly
90+
foreach (['sodium', 'native', 'pbkdf2', $config['hash_algorithm']] as $algo) {
91+
if ('sodium' !== $algo || SodiumPasswordEncoder::isSupported()) {
92+
$config['algorithm'] = $algo;
93+
$encoderChain[] = $this->createEncoder($config);
94+
}
95+
}
96+
97+
return [
98+
'class' => MigratingPasswordEncoder::class,
99+
'arguments' => $encoderChain,
100+
];
89101
}
90102

91103
switch ($config['algorithm']) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
/**
15+
* Hashes passwords using the best available encoder.
16+
* Validates them using a chain of encoders.
17+
*
18+
* /!\ Don't put a PlaintextPasswordEncoder in the list as that'd mean a leaked hash
19+
* could be used to authenticate successfully without knowing the cleartext password.
20+
*
21+
* @author Nicolas Grekas <p@tchwork.com>
22+
*/
23+
final class MigratingPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface
24+
{
25+
private $bestEncoder;
26+
private $extraEncoders;
27+
28+
public function __construct(PasswordEncoderInterface $bestEncoder, PasswordEncoderInterface ...$extraEncoders)
29+
{
30+
$this->bestEncoder = $bestEncoder;
31+
$this->extraEncoders = $extraEncoders;
32+
}
33+
34+
/**
35+
* {@inheritdoc}
36+
*/
37+
public function encodePassword($raw, $salt)
38+
{
39+
return $this->bestEncoder->encodePassword($raw, $salt);
40+
}
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
public function isPasswordValid($encoded, $raw, $salt)
46+
{
47+
if ($this->bestEncoder->isPasswordValid($encoded, $raw, $salt)) {
48+
return true;
49+
}
50+
51+
if (!$this->bestEncoder->needsRehash($encoded)) {
52+
return false;
53+
}
54+
55+
foreach ($this->extraEncoders as $encoder) {
56+
if ($encoder->isPasswordValid($encoded, $raw, $salt)) {
57+
return true;
58+
}
59+
}
60+
61+
return false;
62+
}
63+
64+
/**
65+
* {@inheritdoc}
66+
*/
67+
public function needsRehash(string $encoded): bool
68+
{
69+
return $this->bestEncoder->needsRehash($encoded);
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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\Tests\Encoder;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Security\Core\Encoder\MigratingPasswordEncoder;
16+
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
17+
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
18+
19+
class MigratingPasswordEncoderTest extends TestCase
20+
{
21+
public function testValidation()
22+
{
23+
$bestEncoder = new NativePasswordEncoder(4, 12000, 4);
24+
25+
$extraEncoder = $this->getMockBuilder(TestPasswordEncoderInterface::class)->getMock();
26+
$extraEncoder->expects($this->never())->method('encodePassword');
27+
$extraEncoder->expects($this->never())->method('isPasswordValid');
28+
$extraEncoder->expects($this->never())->method('needsRehash');
29+
30+
$encoder = new MigratingPasswordEncoder($bestEncoder, $extraEncoder);
31+
32+
$this->assertTrue($encoder->needsRehash('foo'));
33+
34+
$hash = $encoder->encodePassword('foo', 'salt');
35+
$this->assertFalse($encoder->needsRehash($hash));
36+
37+
$this->assertTrue($encoder->isPasswordValid($hash, 'foo', 'salt'));
38+
$this->assertFalse($encoder->isPasswordValid($hash, 'bar', 'salt'));
39+
}
40+
41+
public function testFallback()
42+
{
43+
$bestEncoder = new NativePasswordEncoder(4, 12000, 4);
44+
45+
$extraEncoder1 = $this->getMockBuilder(TestPasswordEncoderInterface::class)->getMock();
46+
$extraEncoder1->expects($this->any())
47+
->method('isPasswordValid')
48+
->with('abc', 'foo', 'salt')
49+
->willReturn(true);
50+
;
51+
52+
$encoder = new MigratingPasswordEncoder($bestEncoder, $extraEncoder1);
53+
54+
$this->assertTrue($encoder->isPasswordValid('abc', 'foo', 'salt'));
55+
56+
$extraEncoder2 = $this->getMockBuilder(TestPasswordEncoderInterface::class)->getMock();
57+
$extraEncoder2->expects($this->any())
58+
->method('isPasswordValid')
59+
->willReturn(false);
60+
;
61+
62+
$encoder = new MigratingPasswordEncoder($bestEncoder, $extraEncoder2);
63+
64+
$this->assertFalse($encoder->isPasswordValid('abc', 'foo', 'salt'));
65+
66+
$encoder = new MigratingPasswordEncoder($bestEncoder, $extraEncoder2, $extraEncoder1);
67+
68+
$this->assertTrue($encoder->isPasswordValid('abc', 'foo', 'salt'));
69+
}
70+
}
71+
72+
interface TestPasswordEncoderInterface extends PasswordEncoderInterface
73+
{
74+
public function needsRehash(string $encoded): bool;
75+
}

0 commit comments

Comments
 (0)