Skip to content

Commit 84dedcb

Browse files
committed
Add support for the password-hasher component
1 parent 13cf3b1 commit 84dedcb

File tree

7 files changed

+215
-4
lines changed

7 files changed

+215
-4
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSUserBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.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 FOS\UserBundle\DependencyInjection\Compiler;
13+
14+
use FOS\UserBundle\Util\PasswordUpdater;
15+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\Reference;
18+
19+
/**
20+
* @internal
21+
*/
22+
final class ConfigurePasswordHasherPass implements CompilerPassInterface
23+
{
24+
/**
25+
* {@inheritdoc}
26+
*/
27+
public function process(ContainerBuilder $container)
28+
{
29+
if ($container->has('security.password_hasher_factory')) {
30+
return;
31+
}
32+
33+
// If we don't have the new service for password-hasher, use the old implementation based on the EncoderFactoryInterface
34+
$def = $container->getDefinition('fos_user.util.password_updater');
35+
36+
$def->setClass(PasswordUpdater::class);
37+
$def->setArgument(0, new Reference('security.encoder_factory'));
38+
}
39+
}

FOSUserBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\DoctrineMongoDBMappingsPass;
1717
use FOS\UserBundle\DependencyInjection\Compiler\CheckForMailerPass;
1818
use FOS\UserBundle\DependencyInjection\Compiler\CheckForSessionPass;
19+
use FOS\UserBundle\DependencyInjection\Compiler\ConfigurePasswordHasherPass;
1920
use FOS\UserBundle\DependencyInjection\Compiler\InjectRememberMeServicesPass;
2021
use FOS\UserBundle\DependencyInjection\Compiler\InjectUserCheckerPass;
2122
use FOS\UserBundle\DependencyInjection\Compiler\ValidationPass;
@@ -33,6 +34,7 @@ class FOSUserBundle extends Bundle
3334
public function build(ContainerBuilder $container)
3435
{
3536
parent::build($container);
37+
$container->addCompilerPass(new ConfigurePasswordHasherPass());
3638
$container->addCompilerPass(new ValidationPass());
3739
$container->addCompilerPass(new InjectUserCheckerPass());
3840
$container->addCompilerPass(new InjectRememberMeServicesPass());

Resources/config/util.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818

1919
<service id="FOS\UserBundle\Util\TokenGeneratorInterface" alias="fos_user.util.token_generator" public="false" />
2020

21-
<service id="fos_user.util.password_updater" class="FOS\UserBundle\Util\PasswordUpdater" public="false">
22-
<argument type="service" id="security.encoder_factory" />
21+
<service id="fos_user.util.password_updater" class="FOS\UserBundle\Util\HashingPasswordUpdater" public="false">
22+
<argument type="service" id="security.password_hasher_factory" />
2323
</service>
2424

2525
<service id="FOS\UserBundle\Util\PasswordUpdaterInterface" alias="fos_user.util.password_updater" public="false" />
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSUserBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.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 FOS\UserBundle\Tests\Util;
13+
14+
use FOS\UserBundle\Tests\TestUser;
15+
use FOS\UserBundle\Util\HashingPasswordUpdater;
16+
use PHPUnit\Framework\TestCase;
17+
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
18+
use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
19+
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
20+
21+
class HashingPasswordUpdaterTest extends TestCase
22+
{
23+
/**
24+
* @var HashingPasswordUpdater
25+
*/
26+
private $updater;
27+
private $passwordHasherFactory;
28+
29+
protected function setUp(): void
30+
{
31+
if (!interface_exists(PasswordHasherFactoryInterface::class)) {
32+
self::markTestSkipped('This test requires having the password-hasher component.');
33+
}
34+
35+
$this->passwordHasherFactory = $this->getMockBuilder(PasswordHasherFactoryInterface::class)->getMock();
36+
37+
$this->updater = new HashingPasswordUpdater($this->passwordHasherFactory);
38+
}
39+
40+
public function testUpdatePassword()
41+
{
42+
$hasher = $this->getMockBuilder(PasswordHasherInterface::class)->getMock();
43+
$user = new TestUser();
44+
$user->setPlainPassword('password');
45+
46+
$this->passwordHasherFactory->expects($this->once())
47+
->method('getPasswordHasher')
48+
->with($user)
49+
->will($this->returnValue($hasher));
50+
51+
$hasher->expects($this->once())
52+
->method('hash')
53+
->with('password', $this->identicalTo(null))
54+
->will($this->returnValue('hashedPassword'));
55+
56+
$this->updater->hashPassword($user);
57+
$this->assertSame('hashedPassword', $user->getPassword(), '->updatePassword() sets hashed password');
58+
$this->assertNull($user->getSalt());
59+
$this->assertNull($user->getPlainPassword(), '->updatePassword() erases credentials');
60+
}
61+
62+
public function testUpdatePasswordWithLegacyHasher()
63+
{
64+
$hasher = $this->getMockBuilder(LegacyPasswordHasherInterface::class)->getMock();
65+
$user = new TestUser();
66+
$user->setPlainPassword('password');
67+
$user->setSalt('old_salt');
68+
69+
$this->passwordHasherFactory->expects($this->once())
70+
->method('getPasswordHasher')
71+
->with($user)
72+
->will($this->returnValue($hasher));
73+
74+
$hasher->expects($this->once())
75+
->method('hash')
76+
->with('password', $this->isType('string'))
77+
->will($this->returnValue('hashedPassword'));
78+
79+
$this->updater->hashPassword($user);
80+
$this->assertSame('hashedPassword', $user->getPassword(), '->updatePassword() sets hashed password');
81+
$this->assertNotNull($user->getSalt());
82+
$this->assertNull($user->getPlainPassword(), '->updatePassword() erases credentials');
83+
}
84+
85+
public function testDoesNotUpdateWithEmptyPlainPassword()
86+
{
87+
$user = new TestUser();
88+
$user->setPassword('hash');
89+
90+
$user->setPlainPassword('');
91+
92+
$this->updater->hashPassword($user);
93+
$this->assertSame('hash', $user->getPassword());
94+
}
95+
96+
public function testDoesNotUpdateWithoutPlainPassword()
97+
{
98+
$user = new TestUser();
99+
$user->setPassword('hash');
100+
101+
$user->setPlainPassword(null);
102+
103+
$this->updater->hashPassword($user);
104+
$this->assertSame('hash', $user->getPassword());
105+
}
106+
}

Tests/Util/PasswordUpdaterTest.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public function testUpdatePasswordWithBCrypt()
8181
$this->assertNull($user->getPlainPassword(), '->updatePassword() erases credentials');
8282
}
8383

84-
public function testDoesNotUpdateWithoutPlainPassword()
84+
public function testDoesNotUpdateWithEmptyPlainPassword()
8585
{
8686
$user = new TestUser();
8787
$user->setPassword('hash');
@@ -92,6 +92,17 @@ public function testDoesNotUpdateWithoutPlainPassword()
9292
$this->assertSame('hash', $user->getPassword());
9393
}
9494

95+
public function testDoesNotUpdateWithoutPlainPassword()
96+
{
97+
$user = new TestUser();
98+
$user->setPassword('hash');
99+
100+
$user->setPlainPassword(null);
101+
102+
$this->updater->hashPassword($user);
103+
$this->assertSame('hash', $user->getPassword());
104+
}
105+
95106
private function getMockPasswordEncoder()
96107
{
97108
return $this->getMockBuilder('Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface')->getMock();

Util/HashingPasswordUpdater.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSUserBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.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 FOS\UserBundle\Util;
13+
14+
use FOS\UserBundle\Model\UserInterface;
15+
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
16+
use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
17+
18+
/**
19+
* Class updating the hashed password in the user when there is a new password.
20+
*
21+
* @author Christophe Coevoet <stof@notk.org>
22+
*/
23+
class HashingPasswordUpdater implements PasswordUpdaterInterface
24+
{
25+
private $passwordHasherFactory;
26+
27+
public function __construct(PasswordHasherFactoryInterface $passwordHasherFactory)
28+
{
29+
$this->passwordHasherFactory = $passwordHasherFactory;
30+
}
31+
32+
public function hashPassword(UserInterface $user)
33+
{
34+
$plainPassword = $user->getPlainPassword();
35+
36+
if (null === $plainPassword || '' === $plainPassword) {
37+
return;
38+
}
39+
40+
$hasher = $this->passwordHasherFactory->getPasswordHasher($user);
41+
42+
if (!$hasher instanceof LegacyPasswordHasherInterface) {
43+
$user->setSalt(null);
44+
} else {
45+
$salt = rtrim(str_replace('+', '.', base64_encode(random_bytes(32))), '=');
46+
$user->setSalt($salt);
47+
}
48+
49+
$hashedPassword = $hasher->hash($plainPassword, $user->getSalt());
50+
$user->setPassword($hashedPassword);
51+
$user->eraseCredentials();
52+
}
53+
}

Util/PasswordUpdater.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public function hashPassword(UserInterface $user)
3434
{
3535
$plainPassword = $user->getPlainPassword();
3636

37-
if (0 === strlen($plainPassword)) {
37+
if (null === $plainPassword || '' === $plainPassword) {
3838
return;
3939
}
4040

0 commit comments

Comments
 (0)