Skip to content
Permalink
Browse files

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

765f14c [Security] add MigratingPasswordEncoder
  • Loading branch information...
fabpot committed Jun 4, 2019
2 parents 8d359b2 + 765f14c commit ec9159e074a982861fcf3414dcdb0faf928feaad
@@ -5,6 +5,7 @@ CHANGELOG
-----

* Added method `needsRehash()` to `PasswordEncoderInterface` and `UserPasswordEncoderInterface`
* Added `MigratingPasswordEncoder`

4.3.0
-----
@@ -85,7 +85,17 @@ private function createEncoder(array $config)
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;
}

0 comments on commit ec9159e

Please sign in to comment.
You can’t perform that action at this time.