feature #31597 [Security] add MigratingPasswordEncoder (nicolas-grekas)
This PR was merged into the 4.4 branch.
Discussion
----------
[Security] add MigratingPasswordEncoder
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | -
| License | MIT
| Doc PR | -
Split from #31153: the proposed `MigratingPasswordEncoder` is able to validate password using a chain of encoders, and encodes new them using the best-provided algorithm.
This chained encoder is used when the "auto" algorithm is configured. This is seamless for 4.3 app.
Commits
-------
765f14c80e
[Security] add MigratingPasswordEncoder
This commit is contained in:
commit
ec9159e074
@ -5,6 +5,7 @@ CHANGELOG
|
||||
-----
|
||||
|
||||
* Added method `needsRehash()` to `PasswordEncoderInterface` and `UserPasswordEncoderInterface`
|
||||
* Added `MigratingPasswordEncoder`
|
||||
|
||||
4.3.0
|
||||
-----
|
||||
|
@ -85,7 +85,17 @@ class EncoderFactory implements EncoderFactoryInterface
|
||||
private function getEncoderConfigFromAlgorithm($config)
|
||||
{
|
||||
if ('auto' === $config['algorithm']) {
|
||||
$config['algorithm'] = SodiumPasswordEncoder::isSupported() ? 'sodium' : 'native';
|
||||
$encoderChain = [];
|
||||
// "plaintext" is not listed as any leaked hashes could then be used to authenticate directly
|
||||
foreach ([SodiumPasswordEncoder::isSupported() ? 'sodium' : 'native', 'pbkdf2', $config['hash_algorithm']] as $algo) {
|
||||
$config['algorithm'] = $algo;
|
||||
$encoderChain[] = $this->createEncoder($config);
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => MigratingPasswordEncoder::class,
|
||||
'arguments' => $encoderChain,
|
||||
];
|
||||
}
|
||||
|
||||
switch ($config['algorithm']) {
|
||||
|
@ -0,0 +1,71 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Hashes passwords using the best available encoder.
|
||||
* Validates them using a chain of encoders.
|
||||
*
|
||||
* /!\ Don't put a PlaintextPasswordEncoder in the list as that'd mean a leaked hash
|
||||
* could be used to authenticate successfully without knowing the cleartext password.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class MigratingPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface
|
||||
{
|
||||
private $bestEncoder;
|
||||
private $extraEncoders;
|
||||
|
||||
public function __construct(PasswordEncoderInterface $bestEncoder, PasswordEncoderInterface ...$extraEncoders)
|
||||
{
|
||||
$this->bestEncoder = $bestEncoder;
|
||||
$this->extraEncoders = $extraEncoders;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function encodePassword($raw, $salt)
|
||||
{
|
||||
return $this->bestEncoder->encodePassword($raw, $salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isPasswordValid($encoded, $raw, $salt)
|
||||
{
|
||||
if ($this->bestEncoder->isPasswordValid($encoded, $raw, $salt)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->bestEncoder->needsRehash($encoded)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->extraEncoders as $encoder) {
|
||||
if ($encoder->isPasswordValid($encoded, $raw, $salt)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function needsRehash(string $encoded): bool
|
||||
{
|
||||
return $this->bestEncoder->needsRehash($encoded);
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
<?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\Tests\Encoder;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Encoder\MigratingPasswordEncoder;
|
||||
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
|
||||
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
|
||||
|
||||
class MigratingPasswordEncoderTest extends TestCase
|
||||
{
|
||||
public function testValidation()
|
||||
{
|
||||
$bestEncoder = new NativePasswordEncoder(4, 12000, 4);
|
||||
|
||||
$extraEncoder = $this->getMockBuilder(TestPasswordEncoderInterface::class)->getMock();
|
||||
$extraEncoder->expects($this->never())->method('encodePassword');
|
||||
$extraEncoder->expects($this->never())->method('isPasswordValid');
|
||||
$extraEncoder->expects($this->never())->method('needsRehash');
|
||||
|
||||
$encoder = new MigratingPasswordEncoder($bestEncoder, $extraEncoder);
|
||||
|
||||
$this->assertTrue($encoder->needsRehash('foo'));
|
||||
|
||||
$hash = $encoder->encodePassword('foo', 'salt');
|
||||
$this->assertFalse($encoder->needsRehash($hash));
|
||||
|
||||
$this->assertTrue($encoder->isPasswordValid($hash, 'foo', 'salt'));
|
||||
$this->assertFalse($encoder->isPasswordValid($hash, 'bar', 'salt'));
|
||||
}
|
||||
|
||||
public function testFallback()
|
||||
{
|
||||
$bestEncoder = new NativePasswordEncoder(4, 12000, 4);
|
||||
|
||||
$extraEncoder1 = $this->getMockBuilder(TestPasswordEncoderInterface::class)->getMock();
|
||||
$extraEncoder1->expects($this->any())
|
||||
->method('isPasswordValid')
|
||||
->with('abc', 'foo', 'salt')
|
||||
->willReturn(true);
|
||||
|
||||
$encoder = new MigratingPasswordEncoder($bestEncoder, $extraEncoder1);
|
||||
|
||||
$this->assertTrue($encoder->isPasswordValid('abc', 'foo', 'salt'));
|
||||
|
||||
$extraEncoder2 = $this->getMockBuilder(TestPasswordEncoderInterface::class)->getMock();
|
||||
$extraEncoder2->expects($this->any())
|
||||
->method('isPasswordValid')
|
||||
->willReturn(false);
|
||||
|
||||
$encoder = new MigratingPasswordEncoder($bestEncoder, $extraEncoder2);
|
||||
|
||||
$this->assertFalse($encoder->isPasswordValid('abc', 'foo', 'salt'));
|
||||
|
||||
$encoder = new MigratingPasswordEncoder($bestEncoder, $extraEncoder2, $extraEncoder1);
|
||||
|
||||
$this->assertTrue($encoder->isPasswordValid('abc', 'foo', 'salt'));
|
||||
}
|
||||
}
|
||||
|
||||
interface TestPasswordEncoderInterface extends PasswordEncoderInterface
|
||||
{
|
||||
public function needsRehash(string $encoded): bool;
|
||||
}
|
Reference in New Issue
Block a user