Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@ on:

jobs:
run:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php:
- '7.1'
- '7.2'
- '7.3'
- '7.4'
- '8.0'
- '8.1'
symfony-versions: [false]
include:
- description: 'Symfony 4.*'
Expand Down Expand Up @@ -48,5 +46,4 @@ jobs:

- name: Run PHPUnit tests
run: |
export SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT=1
./vendor/bin/simple-phpunit
39 changes: 39 additions & 0 deletions DependencyInjection/Compiler/ConfigurePasswordHasherPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This file is part of the FOSUserBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FOS\UserBundle\DependencyInjection\Compiler;

use FOS\UserBundle\Util\PasswordUpdater;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
* @internal
*/
final class ConfigurePasswordHasherPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if ($container->has('security.password_hasher_factory')) {
return;
}

// If we don't have the new service for password-hasher, use the old implementation based on the EncoderFactoryInterface
$def = $container->getDefinition('fos_user.util.password_updater');

$def->setClass(PasswordUpdater::class);
$def->setArgument(0, new Reference('security.encoder_factory'));
}
}
2 changes: 2 additions & 0 deletions FOSUserBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\DoctrineMongoDBMappingsPass;
use FOS\UserBundle\DependencyInjection\Compiler\CheckForMailerPass;
use FOS\UserBundle\DependencyInjection\Compiler\CheckForSessionPass;
use FOS\UserBundle\DependencyInjection\Compiler\ConfigurePasswordHasherPass;
use FOS\UserBundle\DependencyInjection\Compiler\InjectRememberMeServicesPass;
use FOS\UserBundle\DependencyInjection\Compiler\InjectUserCheckerPass;
use FOS\UserBundle\DependencyInjection\Compiler\ValidationPass;
Expand All @@ -33,6 +34,7 @@ class FOSUserBundle extends Bundle
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new ConfigurePasswordHasherPass());
$container->addCompilerPass(new ValidationPass());
$container->addCompilerPass(new InjectUserCheckerPass());
$container->addCompilerPass(new InjectRememberMeServicesPass());
Expand Down
7 changes: 6 additions & 1 deletion Model/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ public function getId()
return $this->id;
}

public function getUserIdentifier(): string
{
return $this->username;
}

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -242,7 +247,7 @@ public function getEmailCanonical()
/**
* {@inheritdoc}
*/
public function getPassword()
public function getPassword(): ?string
{
return $this->password;
}
Expand Down
19 changes: 18 additions & 1 deletion Model/UserInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,25 @@

namespace FOS\UserBundle\Model;

use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface as BaseUserInterface;

if (interface_exists(PasswordAuthenticatedUserInterface::class)) {
/**
* @internal Only for back compatibility. Remove / merge when dropping support for Symfony 4
*/
interface CompatUserInterface extends PasswordAuthenticatedUserInterface, BaseUserInterface
{
}
} else {
/**
* @internal Only for back compatibility. Remove / merge when dropping support for Symfony 4
*/
interface CompatUserInterface extends BaseUserInterface
{
}
}

/**
* Implementations of that interface must be serializable. The mechanism
* being used to support serialization is up for the implementation.
Expand All @@ -23,7 +40,7 @@
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @author Julian Finkler <julian@developer-heaven.de>
*/
interface UserInterface extends BaseUserInterface
interface UserInterface extends CompatUserInterface
{
public const ROLE_DEFAULT = 'ROLE_USER';

Expand Down
4 changes: 2 additions & 2 deletions Resources/config/util.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@

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

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

<service id="FOS\UserBundle\Util\PasswordUpdaterInterface" alias="fos_user.util.password_updater" public="false" />
Expand Down
24 changes: 22 additions & 2 deletions Security/UserProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use FOS\UserBundle\Model\UserManagerInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface as SecurityUserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

Expand All @@ -33,6 +34,17 @@ public function __construct(UserManagerInterface $userManager)
$this->userManager = $userManager;
}

public function loadUserByIdentifier(string $identifier): SecurityUserInterface
{
$user = $this->findUser($identifier);

if (!$user) {
throw new UserNotFoundException(sprintf('Username "%s" does not exist.', $identifier));
}

return $user;
}

/**
* {@inheritdoc}
*/
Expand All @@ -41,6 +53,10 @@ public function loadUserByUsername($username)
$user = $this->findUser($username);

if (!$user) {
if (class_exists(UserNotFoundException::class)) {
throw new UserNotFoundException(sprintf('Username "%s" does not exist.', $username));
}

throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
}

Expand All @@ -50,7 +66,7 @@ public function loadUserByUsername($username)
/**
* {@inheritdoc}
*/
public function refreshUser(SecurityUserInterface $user)
public function refreshUser(SecurityUserInterface $user): SecurityUserInterface
{
if (!$user instanceof UserInterface) {
throw new UnsupportedUserException(sprintf('Expected an instance of FOS\UserBundle\Model\UserInterface, but got "%s".', get_class($user)));
Expand All @@ -61,6 +77,10 @@ public function refreshUser(SecurityUserInterface $user)
}

if (null === $reloadedUser = $this->userManager->findUserBy(['id' => $user->getId()])) {
if (class_exists(UserNotFoundException::class)) {
throw new UserNotFoundException(sprintf('User with ID "%s" could not be reloaded.', $user->getId()));
}

throw new UsernameNotFoundException(sprintf('User with ID "%s" could not be reloaded.', $user->getId()));
}

Expand All @@ -70,7 +90,7 @@ public function refreshUser(SecurityUserInterface $user)
/**
* {@inheritdoc}
*/
public function supportsClass($class)
public function supportsClass($class): bool
{
$userClass = $this->userManager->getClass();

Expand Down
2 changes: 1 addition & 1 deletion Tests/Mailer/MailerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

class MailerTest extends TestCase
{
protected function setUp()
protected function setUp(): void
{
// skip test for Symfony > 5
if (!interface_exists('Symfony\Bundle\FrameworkBundle\Templating\EngineInterface')) {
Expand Down
106 changes: 106 additions & 0 deletions Tests/Util/HashingPasswordUpdaterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

/*
* This file is part of the FOSUserBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FOS\UserBundle\Tests\Util;

use FOS\UserBundle\Tests\TestUser;
use FOS\UserBundle\Util\HashingPasswordUpdater;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;

class HashingPasswordUpdaterTest extends TestCase
{
/**
* @var HashingPasswordUpdater
*/
private $updater;
private $passwordHasherFactory;

protected function setUp(): void
{
if (!interface_exists(PasswordHasherFactoryInterface::class)) {
self::markTestSkipped('This test requires having the password-hasher component.');
}

$this->passwordHasherFactory = $this->getMockBuilder(PasswordHasherFactoryInterface::class)->getMock();

$this->updater = new HashingPasswordUpdater($this->passwordHasherFactory);
}

public function testUpdatePassword()
{
$hasher = $this->getMockBuilder(PasswordHasherInterface::class)->getMock();
$user = new TestUser();
$user->setPlainPassword('password');

$this->passwordHasherFactory->expects($this->once())
->method('getPasswordHasher')
->with($user)
->will($this->returnValue($hasher));

$hasher->expects($this->once())
->method('hash')
->with('password', $this->identicalTo(null))
->will($this->returnValue('hashedPassword'));

$this->updater->hashPassword($user);
$this->assertSame('hashedPassword', $user->getPassword(), '->updatePassword() sets hashed password');
$this->assertNull($user->getSalt());
$this->assertNull($user->getPlainPassword(), '->updatePassword() erases credentials');
}

public function testUpdatePasswordWithLegacyHasher()
{
$hasher = $this->getMockBuilder(LegacyPasswordHasherInterface::class)->getMock();
$user = new TestUser();
$user->setPlainPassword('password');
$user->setSalt('old_salt');

$this->passwordHasherFactory->expects($this->once())
->method('getPasswordHasher')
->with($user)
->will($this->returnValue($hasher));

$hasher->expects($this->once())
->method('hash')
->with('password', $this->isType('string'))
->will($this->returnValue('hashedPassword'));

$this->updater->hashPassword($user);
$this->assertSame('hashedPassword', $user->getPassword(), '->updatePassword() sets hashed password');
$this->assertNotNull($user->getSalt());
$this->assertNull($user->getPlainPassword(), '->updatePassword() erases credentials');
}

public function testDoesNotUpdateWithEmptyPlainPassword()
{
$user = new TestUser();
$user->setPassword('hash');

$user->setPlainPassword('');

$this->updater->hashPassword($user);
$this->assertSame('hash', $user->getPassword());
}

public function testDoesNotUpdateWithoutPlainPassword()
{
$user = new TestUser();
$user->setPassword('hash');

$user->setPlainPassword(null);

$this->updater->hashPassword($user);
$this->assertSame('hash', $user->getPassword());
}
}
13 changes: 12 additions & 1 deletion Tests/Util/PasswordUpdaterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public function testUpdatePasswordWithBCrypt()
$this->assertNull($user->getPlainPassword(), '->updatePassword() erases credentials');
}

public function testDoesNotUpdateWithoutPlainPassword()
public function testDoesNotUpdateWithEmptyPlainPassword()
{
$user = new TestUser();
$user->setPassword('hash');
Expand All @@ -92,6 +92,17 @@ public function testDoesNotUpdateWithoutPlainPassword()
$this->assertSame('hash', $user->getPassword());
}

public function testDoesNotUpdateWithoutPlainPassword()
{
$user = new TestUser();
$user->setPassword('hash');

$user->setPlainPassword(null);

$this->updater->hashPassword($user);
$this->assertSame('hash', $user->getPassword());
}

private function getMockPasswordEncoder()
{
return $this->getMockBuilder('Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface')->getMock();
Expand Down
Loading