Skip to content

Instantly share code, notes, and snippets.

@simshaun
Last active July 24, 2019 17:58
Show Gist options
  • Save simshaun/89407e39c7c6ef66268fd5327ea8a6a1 to your computer and use it in GitHub Desktop.
Save simshaun/89407e39c7c6ef66268fd5327ea8a6a1 to your computer and use it in GitHub Desktop.
Decoupling App User entity from Symfony Security User. Public props and clipped methods for brevity.
<?php
# src/DataProvider/CurrentUserProvider.php
namespace App\DataProvider;
use App\Entity\User;
use App\Repository\UserRepository;
use App\Security\SecurityUser;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
final class CurrentUserProvider
{
private $tokenStorage;
private $userRepo;
private $currentUser; // cached to prevent querying multiple times
public function __construct(TokenStorageInterface $tokenStorage, UserRepository $userRepo)
{
$this->tokenStorage = $tokenStorage;
$this->userRepo = $userRepo;
}
public function get(): ?User
{
if (!$this->currentUser) {
$this->currentUser = $this->fromToken($this->tokenStorage->getToken());
}
return $this->currentUser;
}
public function fromToken(TokenInterface $token): ?User
{
if (!$token || !$token->getUser() instanceof SecurityUser) {
return null;
}
return $this->userRepo->findOneByEmail($token->getUsername());
}
}
<?php
# src/Security/LoginFormAuthenticator.php
# Methods clipped for brevity.
namespace App\Security;
use App\DataProvider\CurrentUserProvider;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
private $currentUserProvider;
public function __construct(CurrentUserProvider $currentUserProvider)
{
$this->currentUserProvider = $currentUserProvider;
}
public function supports(Request $request): bool
{
// ...
}
public function getCredentials(Request $request)
{
// ...
}
public function getUser($credentials, UserProviderInterface $userProvider): UserInterface
{
// ...
try {
$user = $userProvider->loadUserByUsername($credentials['email']);
} catch (UsernameNotFoundException $exception) {
throw new CustomUserMessageAuthenticationException('Email could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user): bool
{
// ...
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$user = $this->currentUserProvider->fromToken($token);
// ...
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
// ...
}
public function start(Request $request, AuthenticationException $authException = null)
{
// ...
}
protected function getLoginUrl()
{
// ...
}
}
# config/packages/security.yaml
security:
encoders:
# Both must use the same encoder
App\Entity\User: bcrypt
App\Security\SecurityUser: bcrypt
providers:
custom_provider:
id: App\Security\SecurityUserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: ~
provider: custom_provider
guard:
authenticators:
- App\Security\LoginFormAuthenticator
# access_control:
# - { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
<?php
# src/Security/SecurityUser.php
namespace App\Security;
use App\Entity\User;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\UserInterface;
final class SecurityUser implements UserInterface, EquatableInterface
{
public $id;
public $email;
public $password;
public $admin;
public function __construct(User $user)
{
$this->id = $user->getId();
$this->email = $user->email;
$this->password = $user->password;
$this->admin = $user->admin;
}
public function getRoles(): array
{
if ($this->admin) {
return ['ROLE_ADMIN'];
}
return [];
}
public function getPassword(): ?string
{
return $this->password;
}
public function getSalt(): ?string
{
return null;
}
public function getUsername(): string
{
return $this->email;
}
public function eraseCredentials(): void
{
$this->password = null;
}
public function isEqualTo(UserInterface $user): bool
{
if (!$user instanceof self) {
return false;
}
if ($this->getRoles() !== $user->getRoles()) {
return false;
}
return true;
}
}
<?php
# src/Security/SecurityUserProvider.php
namespace App\Security;
use App\Repository\UserRepository;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
final class SecurityUserProvider implements UserProviderInterface
{
private $userRepo;
public function __construct(UserRepository $userRepo)
{
$this->userRepo = $userRepo;
}
public function supportsClass($class): bool
{
return $class === SecurityUser::class;
}
public function loadUserByUsername($username): UserInterface
{
if (null === ($user = $this->userRepo->findOneByEmail($username))) {
throw new UsernameNotFoundException(sprintf('No user found for "%s"', $username));
}
return new SecurityUser($user);
}
public function refreshUser(UserInterface $user): UserInterface
{
if (!$user instanceof SecurityUser) {
throw new UnsupportedUserException(sprintf('Invalid user class %s', \get_class($user)));
}
$userEntity = $this->userRepo->findOneByEmail($user->getUsername());
if (!$userEntity) {
throw new UsernameNotFoundException(sprintf('No user found for "%s"', $user->getUsername()));
}
return new SecurityUser($userEntity);
}
}
<?php
# src/Entity/User.php
namespace App\Entity;
class User
{
private $id;
public $name = '';
public $email = '';
public $plainPassword = '';
public $password = '';
public $admin = false;
public function __construct(string $name, string $email, string $plainPassword)
{
$this->name = $name;
$this->email = $email;
$this->plainPassword = $plainPassword;
}
public function getId(): int
{
return $this->id;
}
}
@jessequinn
Copy link

Nice work, just a quick question, can you demonstrate how you are using the onAuthenticationSuccess with $user = $this->currentUserProvider->fromToken($token);

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
//        $user = $this->currentUserProvider->fromToken($token);

        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
            return new RedirectResponse($targetPath);
        }

        // For example : return new RedirectResponse($this->urlGenerator->generate('some_route'));
//        throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
        return new RedirectResponse($this->urlGenerator->generate('app_dashboard'));
    }

@simshaun
Copy link
Author

Nice work, just a quick question, can you demonstrate how you are using the onAuthenticationSuccess with $user = $this->currentUserProvider->fromToken($token);

It depends on the application, but this is an example from one I have open:

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        $targetPath = $request->hasSession() ? $request->getSession()->get('_security.main.target_path') : null;

        if ($request->isXmlHttpRequest()) {
            return new JsonResponse([
                'username' => $token->getUsername(),
                'target_path' => $targetPath,
                'csrf_token' => $this->csrfTokenManager->getToken('authenticate'),
            ]);
        }

        if ($targetPath) {
            return new RedirectResponse($targetPath);
        }

        return new RedirectResponse($this->router->generate('user_dashboard'));
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment