Last active
July 24, 2019 17:58
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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()); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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() | |
{ | |
// ... | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
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
Nice work, just a quick question, can you demonstrate how you are using the
onAuthenticationSuccess
with$user = $this->currentUserProvider->fromToken($token);