Skip to content

Commit

Permalink
Implemented UserProviderInterface to UserRepository.
Browse files Browse the repository at this point in the history
  • Loading branch information
laurentmuller committed Dec 8, 2024
1 parent b68dd29 commit 17d54ac
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 28 deletions.
8 changes: 4 additions & 4 deletions config/packages/security.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use App\Entity\User;
use App\Interfaces\RoleInterface;
use App\Listener\ResponseListener;
use App\Repository\UserRepository;
use App\Security\LoginFormAuthenticator;
use App\Security\SecurityAttributes;
use Symfony\Component\HttpFoundation\Cookie;
Expand All @@ -33,10 +34,8 @@
->roleHierarchy(RoleInterface::ROLE_SUPER_ADMIN, [RoleInterface::ROLE_ADMIN]);

// user provider
$config->provider('app_user_provider')
->entity()
->class(User::class)
->property('username');
$config->provider('user_provider')
->id(UserRepository::class);

// dev firewall
$config->firewall(ResponseListener::FIREWALL_DEV)
Expand All @@ -47,6 +46,7 @@
$firewall = $config->firewall(ResponseListener::FIREWALL_MAIN)
->customAuthenticators([LoginFormAuthenticator::class])
->entryPoint(LoginFormAuthenticator::class)
->provider('user_provider')
->lazy(true);

// allows 5 login attempts per minute
Expand Down
44 changes: 43 additions & 1 deletion src/Repository/UserRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface;
use SymfonyCasts\Bundle\ResetPassword\Persistence\Repository\ResetPasswordRequestRepositoryTrait;
use SymfonyCasts\Bundle\ResetPassword\Persistence\ResetPasswordRequestRepositoryInterface;
Expand All @@ -29,8 +33,10 @@
* Repository for user entity.
*
* @template-extends AbstractRepository<User>
*
* @implements UserProviderInterface<User>
*/
class UserRepository extends AbstractRepository implements PasswordUpgraderInterface, ResetPasswordRequestRepositoryInterface
class UserRepository extends AbstractRepository implements PasswordUpgraderInterface, ResetPasswordRequestRepositoryInterface, UserProviderInterface
{
use ResetPasswordRequestRepositoryTrait;

Expand Down Expand Up @@ -174,6 +180,34 @@ public function isResettableUsers(): bool
->getSingleScalarResult();
}

/**
* @see UserProviderInterface
*/
public function loadUserByIdentifier(string $identifier): User
{
$user = $this->findByUsername($identifier);
if (!$user instanceof User) {
$e = new UserNotFoundException(\sprintf('User "%s" not found.', $identifier));
$e->setUserIdentifier($identifier);

throw $e;
}

return $user;
}

/**
* @see UserProviderInterface
*/
public function refreshUser(UserInterface $user): User
{
if (!$user instanceof User) {
throw new UnsupportedUserException(\sprintf('Instances of "%s" are not supported.', \get_debug_type($user)));
}

return $this->loadUserByIdentifier($user->getUsername());
}

/**
* @see ResetPasswordRequestRepositoryInterface
*/
Expand Down Expand Up @@ -220,6 +254,14 @@ public function resetPasswordRequest(User|array $users): void
$this->flush();
}

/**
* @see UserProviderInterface
*/
public function supportsClass(string $class): bool
{
return User::class === $class;
}

/**
* @see PasswordUpgraderInterface
*
Expand Down
20 changes: 6 additions & 14 deletions src/Security/LoginFormAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace App\Security;

use App\Repository\UserRepository;
use App\Service\ApplicationService;
use App\Service\CaptchaImageService;
use Symfony\Component\HttpFoundation\Request;
Expand All @@ -21,9 +22,6 @@
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
Expand All @@ -36,13 +34,10 @@

class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
/**
* @param UserProviderInterface<UserInterface> $userProvider
*/
public function __construct(
private readonly ApplicationService $applicationService,
private readonly CaptchaImageService $captchaImageService,
private readonly UserProviderInterface $userProvider,
private readonly UserRepository $repository,
private readonly HttpUtils $httpUtils,
) {
}
Expand All @@ -51,19 +46,16 @@ public function authenticate(Request $request): Passport
{
$this->validateCaptcha($request);
$credentials = $this->getCredentials($request);
$passport = new Passport(
new UserBadge($credentials[SecurityAttributes::USER_FIELD], $this->userProvider->loadUserByIdentifier(...)),

return new Passport(
new UserBadge($credentials[SecurityAttributes::USER_FIELD], $this->repository->loadUserByIdentifier(...)),
new PasswordCredentials($credentials[SecurityAttributes::PASSWORD_FIELD]),
[
new RememberMeBadge(),
new PasswordUpgradeBadge($credentials[SecurityAttributes::PASSWORD_FIELD], $this->repository),
new CsrfTokenBadge(SecurityAttributes::AUTHENTICATE_TOKEN, $credentials[SecurityAttributes::LOGIN_TOKEN]),
]
);
if ($this->userProvider instanceof PasswordUpgraderInterface) {
$passport->addBadge(new PasswordUpgradeBadge($credentials[SecurityAttributes::PASSWORD_FIELD], $this->userProvider));
}

return $passport;
}

public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
Expand Down
35 changes: 35 additions & 0 deletions tests/Repository/UserRepositoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
use App\Tests\KernelServiceTestCase;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\InMemoryUser;
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface;

class UserRepositoryTest extends KernelServiceTestCase
Expand Down Expand Up @@ -125,6 +128,32 @@ public function testIsResettableUsers(): void
self::assertFalse($actual);
}

public function testLoadUserByIdentifier(): void
{
$user = $this->repository->loadUserByIdentifier('ROLE_USER');
self::assertInstanceOf(User::class, $user);
}

public function testLoadUserByIdentifierException(): void
{
self::expectException(UserNotFoundException::class);
$this->repository->loadUserByIdentifier('FAKE');
}

public function testRefreshUser(): void
{
$user = $this->getUser();
$refreshedUser = $this->repository->refreshUser($user);
self::assertInstanceOf(User::class, $refreshedUser);
}

public function testRefreshUserException(): void
{
self::expectException(UnsupportedUserException::class);
$user = new InMemoryUser('username', 'password', ['ROLE_USER']);
$this->repository->refreshUser($user);
}

public function testRemoveExpiredResetPasswordRequests(): void
{
$actual = $this->repository->removeExpiredResetPasswordRequests();
Expand All @@ -138,6 +167,12 @@ public function testRemoveResetPasswordRequest(): void
self::assertTrue($user->isExpired());
}

public function testSupportsClass(): void
{
self::assertTrue($this->repository->supportsClass(User::class));
self::assertFalse($this->repository->supportsClass(InMemoryUser::class));
}

public function testUpgradePassword(): void
{
$user = $this->getUser();
Expand Down
15 changes: 6 additions & 9 deletions tests/Security/LoginFormAuthenticatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace App\Tests\Security;

use App\Repository\UserRepository;
use App\Security\LoginFormAuthenticator;
use App\Security\SecurityAttributes;
use App\Service\ApplicationService;
Expand All @@ -29,8 +30,6 @@
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\HttpUtils;

Expand Down Expand Up @@ -134,8 +133,8 @@ public function testAuthenticateNoToken(): void
*/
public function testAuthenticateWithUserProvider(): void
{
$userProvider = $this->createMock(ProviderInterface::class);
$authenticator = $this->createAuthenticator(userProvider: $userProvider);
$userProvider = $this->createMock(UserRepository::class);
$authenticator = $this->createAuthenticator(repository: $userProvider);

$values = [
SecurityAttributes::USER_FIELD => 'username',
Expand Down Expand Up @@ -273,25 +272,23 @@ public function testSupports(Request $request, bool $expected): void
}

/**
* @param UserProviderInterface<UserInterface>|null $userProvider
*
* @throws Exception
*/
private function createAuthenticator(
?ApplicationService $applicationService = null,
?CaptchaImageService $captchaImageService = null,
?UserProviderInterface $userProvider = null,
?UserRepository $repository = null,
?HttpUtils $httpUtils = null
): LoginFormAuthenticator {
$applicationService ??= $this->createMock(ApplicationService::class);
$captchaImageService ??= $this->createMock(CaptchaImageService::class);
$userProvider ??= $this->createMock(UserProviderInterface::class);
$repository ??= $this->createMock(UserRepository::class);
$httpUtils ??= $this->createMock(HttpUtils::class);

return new LoginFormAuthenticator(
$applicationService,
$captchaImageService,
$userProvider,
$repository,
$httpUtils,
);
}
Expand Down

0 comments on commit 17d54ac

Please sign in to comment.