Symfony 5: How to get isVerified User in authenticator - symfony

i'm still a beginner in symfony so i hope my question will be fastly answered. I did a lot of research on internet without finding anything about my problem.
I use Symfony 5.3 basic authenticator which is generated when we do the bin/console make:auth with maker bundle. All basic config, i didn't touch anything in security.yaml. The thing i want to perform is set a authentication error if the user is not verified by Email. My User entity is very basic too with make:user. But it seems like the User representation we get with this token in onAuthenticationSuccess doesn't contain any "isVerified" method, an interesting fact is that he doesn't contain any "getEmail" method too, seems like he just contain the main accessors (username, password). Any solution ?
(Login is actually working, i just can't perform this verifying trick.)
EDIT: Just saw that "isVerifed" and "getEmail" methods can't be called from any $this->getUser() in controllers too. But when we do a new User(), we can call these methods.
EDIT AGAIN: Ok it's because $this->getUser() and $token->getUser() extends UserInterface and there's not any getEmail() or isVerified() method in this interface. So what is the trick ? I keep investigating.
Authenticator:
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class Authenticator extends AbstractLoginFormAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
private UrlGeneratorInterface $urlGenerator;
public function __construct(UrlGeneratorInterface $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
public function authenticate(Request $request): PassportInterface
{
$parameters = json_decode($request->getContent(), true);
return new Passport(
new UserBadge($parameters['username']),
new PasswordCredentials($parameters['password'])
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if (!$token->getUser()->isVerified()) { //isVerified show "undefined method"
$request->getSession()->set(Security::AUTHENTICATION_ERROR, "You are not verified. Check your emails.");
}
return null;
}
protected function getLoginUrl(Request $request): string
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
}
My User:
<?php
namespace App\Entity;
use App\Repository\UserSymfangularRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Entity(repositoryClass=UserSymfangularRepository::class)
* #UniqueEntity(fields={"username"}, message="There is already an account with this username")
*/
class UserSymfangular implements UserInterface, PasswordAuthenticatedUserInterface
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
private $username;
/**
* #ORM\Column(type="json")
*/
private $roles = [];
/**
* #var string The hashed password
* #ORM\Column(type="string")
*/
private $password;
/**
* #ORM\Column(type="string", length=255)
*/
private $email;
/**
* #ORM\Column(type="boolean")
*/
private $isVerified = false;
public function getId(): ?int
{
return $this->id;
}
/**
* #deprecated since Symfony 5.3, use getUserIdentifier instead
*/
public function getUsername(): string
{
return (string) $this->username;
}
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
/**
* A visual identifier that represents this user.
*
* #see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->username;
}
/**
* #see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* #see PasswordAuthenticatedUserInterface
*/
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* Returning a salt is only needed, if you are not using a modern
* hashing algorithm (e.g. bcrypt or sodium) in your security.yaml.
*
* #see UserInterface
*/
public function getSalt(): ?string
{
return null;
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
public function getIsVerified(): ?bool
{
return $this->isVerified;
}
public function setIsVerified(bool $isVerified): self
{
$this->isVerified = $isVerified;
return $this;
}
public function isVerified(): bool
{
return $this->isVerified;
}
}
EDIT: Solved it. Actually i can do $this->getUser()->getEmail() or $this->getUser()->isVerified(), but the methods just doesn't appear in the autocomplete of my VSCode. Sorry for all of this.

Related

UserPasswordHasherInterface Symfony 6.1.2

i'm new in this beautiful world :) .
I here because i have a question about symfony.
I have to make a project for my exams, but i'm stuck in hashing password.
I follow the documentation here : https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
But i don't know how to declare the $plaintextPassword.
This is my security.yaml
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component#\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
This is my user entity:
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Entity(repositoryClass=UserRepository::class)
*/
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
private $email;
/**
* #ORM\Column(type="json")
*/
private $roles = [];
/**
* #ORM\Column(type="string")
*/
private $password;
/**
* #ORM\Column(type="string", length=255)
*/
private $firstname;
/**
* #ORM\Column(type="string", length=255)
*/
private $lastname;
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* #see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->email;
}
/**
* #see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* #see PasswordAuthenticatedUserInterface
*/
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function getFirstname(): ?string
{
return $this->firstname;
}
public function setFirstname(string $firstname): self
{
$this->firstname = $firstname;
return $this;
}
public function getLastname(): ?string
{
return $this->lastname;
}
public function setLastname(string $lastname): self
{
$this->lastname = $lastname;
return $this;
}
}
and this is my RegisterController :
namespace App\Controller;
use App\Entity\User;
use App\Form\RegisterType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class RegisterController extends AbstractController
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager){
$this->entityManager = $entityManager;
}
/**
* #Route("/inscription", name="register")
*/
public function index(Request $request, UserPasswordHasherInterface $passwordHasher):
Response
{
$user = new User();
$form = $this->createForm(RegisterType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()){
$user = $form->getData();
/* $password = $passwordHasher->hashPassword($user,$user->getPassword());
$user->setPassword($hashedpassword); */
$hashedPassword = $passwordHasher->hashPassword(
$user,
$plaintextPassword
);
$user->setPassword($hashedPassword);
$this->entityManager->persist($user);
$this->entityManager->flush();
/* dd($user); */
}
return $this->render('url/inscription.html.twig', [
'form' => $form->createView()
]);
}
}
Until then everything was fine, but i don't understand how to use the UserPasswordHasherInterface service, if someone can help me to know how to declare this variable $plaintextpassword to encode my password in database ..
Thanks :)
well,
i found the solution.
$plaintextPassword is a random variable to catch the normal password without being encoded.
Then, i found this documentation : https://symfony.com/doc/current/security/passwords.html
I just forgot to call composer require symfony/password-hasher .
Right now my passwords are encoded in my db :D

Sonata, Symfony, Password Encryption, Doctrine Listerner/Subscriber

Infos : I'm new to Symfony and Sonata
My objective : Encrypte the password only for the database side. I would be able to display this password in clear. All this for the field password in the entity Service.
What did i'm trying ? : I tryed to create a Doctrine Listener which use bcrypt encryption
security:
encoders:
App\Entity\Service: bcrypt
here is the HashPasswordLisetener.php in my App\Doctrine (in the $formMapper of my configureFormField function in App\Admin\ServiceAdmin.php i have a row like this ->add('password'))
<?php
namespace App\Doctrine;
use App\Entity\Service;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class HashPasswordListener implements EventSubscriber
{
private $passwordEncoder;
public function __construct(UserPasswordEncoderInterface $passwordEncoder)
{
$this->passwordEncoder = $passwordEncoder;
}
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof Service) {
return;
}
$this->encodePassword($entity);
}
public function preUpdate(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof Service) {
return;
}
$this->encodePassword($entity);
$em = $args->getEntityManager();
$meta = $em->getClassMetadata(get_class($entity));
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $entity);
}
public function getSubscribedEvents()
{
return ['prePersist', 'preUpdate'];
}
/**
* #param Service $entity
*/
private function encodePassword(Service $entity)
{
if (!$entity->getPlainPassword()) {
return;
}
$encoded = $this->passwordEncoder->encodePassword(
$entity,
$entity->getPlainPassword()
);
$entity->setPassword($encoded);
}
}
here is the Service entity in the App\Entity
<?php
namespace App\Entity;
use App\Admin\AbstractAdmin;
use App\Repository\ServiceRepository;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Timestampable\Traits\TimestampableEntity;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Entity(repositoryClass=ServiceRepository::class)
*/
class Service implements UserInterface
{
use TimestampableEntity;
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
/**
* #ORM\Column(type="string", length=255)
*/
private $password;
/**
* #ORM\Column(type="text", nullable=true, length=255)
*/
private $comment;
/**
* #ORM\Column(type="string", length=255)
*/
private $identifier;
private $plainPassword;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
public function getComment(): ?string
{
return $this->comment;
}
public function setComment(?string $comment): self
{
$this->comment = $comment;
return $this;
}
public function getIdentifier(): ?string
{
return $this->identifier;
}
public function setIdentifier(string $identifier): self
{
$this->identifier = $identifier;
return $this;
}
/**
* #return mixed
*/
public function getPlainPassword()
{
return $this->plainPassword;
}
/**
* #param mixed $plainPassword
*/
public function setPlainPassword($plainPassword): void
{
$this->plainPassword = $plainPassword;
$this->password = null;
}
public function getRoles()
{
return array('ROLE_USER');
}
public function getSalt()
{
}
public function eraseCredentials()
{
$this->plainPassword = null;
}
public function getUsername()
{
return $this->identifier;
}
}
Also my sonata_admin.yaml :
app.doctrine.hash_password_listener:
class: App\Doctrine\HashPasswordListener
autowire : true
tags:
- { name: doctrine.event_subscriber, connection: 'default' }
The result it give me :
My Probleme :
I understand the function encodePassword is waiting for a UserInterface in 1st arg (instead of my entity) and the password to encrypt in 2nd arg but i don't understand who use this UserInterface ? Where i'm suppose to call it ? to get it ? to send it ?
I think i give lot of details but if i forgot something feel free to notice me ^^
Thanks the time you spend at least reading.
I had a probleme but i was coding an Hashing type of code but i was looking for an encrypting methode. but here is how i deal for having a working hashing :
Step 1 : implementing UserInterface in my Service entity
Step 2 : Replace UserPasswordEncoder for UserPasswordEncoderInterface in the Listener
Step 2.5 : Added all function needed for the UserInterface like eraseCredidential getSalt().. see : this for more details
Step 3 : Adding a getUsername which return this->identifier
Step 4 : using plainPasswords in form field instead of password
Step 5 : added a provider :
app_user:
entity:
class: 'App\Entity\Service'
property: 'identifier'
Step 6 : Using TextType::class for plainPassword form field type + fixing the use with :
Symfony\Component\Form\Extension\Core\Type\TextType
Step 7 : Working
(Special thanks to #msg who helped me a lot)

ManyToMany error : Argument 1 passed to Doctrine\Common\Collections\ArrayCollection::__construct() must be of the type array, object given

I have a problem with type ManyToMany relations entities.
I learn symfony i make maybe something wrong.
I tried many ways (Without foreach ...) but i fall always on errors.
These entities were created whit the CLI.
Thanks for your replys and sorry for my bad english
My entities :
Clients
<?php
namespace App\Entity;
use App\Repository\ClientsRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=ClientsRepository::class)
*/
class Clients
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $nom;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $prenom;
/**
* #ORM\Column(type="string", length=255)
*/
private $mail;
/**
* #ORM\Column(type="datetime")
*/
private $created_at;
/**
* #ORM\Column(type="boolean")
*/
private $actif;
/**
* #ORM\ManyToMany(targetEntity=Services::class, mappedBy="clients")
*/
private $services;
public function __construct()
{
$this->services = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getNom(): ?string
{
return $this->nom;
}
public function setNom(string $nom): self
{
$this->nom = $nom;
return $this;
}
public function getPrenom(): ?string
{
return $this->prenom;
}
public function setPrenom(?string $prenom): self
{
$this->prenom = $prenom;
return $this;
}
public function getMail(): ?string
{
return $this->mail;
}
public function setMail(string $mail): self
{
$this->mail = $mail;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->created_at;
}
public function setCreatedAt(\DateTimeInterface $created_at): self
{
$this->created_at = $created_at;
return $this;
}
public function getActif(): ?bool
{
return $this->actif;
}
public function setActif(bool $actif): self
{
$this->actif = $actif;
return $this;
}
/**
* #return Collection|Services[]
*/
public function getServices(): Collection
{
return $this->services;
}
public function addService(Services $service): self
{
if (!$this->services->contains($service)) {
$this->services[] = $service;
$service->setClients($this);
}
return $this;
}
public function removeService(Services $service): self
{
if ($this->services->contains($service)) {
$this->services->removeElement($service);
// set the owning side to null (unless already changed)
if ($service->getClients() === $this) {
$service->setClients(null);
}
}
return $this;
}
}
Services
<?php
namespace App\Entity;
use App\Repository\ServicesRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=ServicesRepository::class)
*/
class Services
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $nom;
/**
* #ORM\Column(type="float")
*/
private $prix;
/**
* #ORM\ManyToMany(targetEntity=Clients::class, inversedBy="services")
*/
private $clients;
public function getId(): ?int
{
return $this->id;
}
public function getNom(): ?string
{
return $this->nom;
}
public function setNom(string $nom): self
{
$this->nom = $nom;
return $this;
}
public function getPrix(): ?float
{
return $this->prix;
}
public function setPrix(float $prix): self
{
$this->prix = $prix;
return $this;
}
public function getClients(): ?Clients
{
return $this->clients;
}
public function setClients(?Clients $clients): self
{
$this->clients = $clients;
return $this;
}
}
Controller
[...]
$entityManager = $this->getDoctrine()->getManager();
foreach($servicesClient as $srv){
$service = $this->getDoctrine()->getRepository(Services::class)->find($srv);
$service->setClients($client);
$client->addService($service);
$entityManager->persist($client);
$entityManager->persist($service);
$entityManager->flush();
}
[...]
Your Service::clients property is marked with #ManyToMany annotation, which means it should be typed #var Collection|Clients[] but both getter and setter returns/set a nullable object ?Clients $clients.
No way the bin/console make:entity did that.
You should use the same system as your Clients::services property, i.e using three methods getClients(), addClients() and removeClients() without setClients(). Besides, you need a constructor starting the $clients property: $this->clients = new ArrayCollection();.
Or maybe you want to use #OneToMany and #ManyToOne annotations.
Note that an entity classname should be singular.

Symfony authenticator: In controllers, Doctrine returns user item with empty string field, although a value is set

My database schema consists mainly of the following entities: user and auth_token. Each user can have multiple auth_token's.
The problem: when selecting the currently authenticated user in a controller, the string field saltedPasswordHash is empty (""), although a value is set in the database. Getting saltedPasswordHash in the ApiKeyAuthenticator.php works (please have a look at the two TODO comments).
For whatever reason, selecting the email (string) or created (datetime) field works. Persisting new user entities with a saltedPasswordHash or selecting any other user item works fine.
An APIKeyAuthenticator is handling the authorization. When disabling firewall and authentication, everything works as expected. I included the source files below.
I'm using PHP 7.2.15-1 with mysql Ver 15.1 Distrib 10.3.13-MariaDB.
Security/ApiKeyAuthenticator.php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
public function createToken(Request $request, $providerKey)
{
$apiKey = $request->headers->get('authToken');
if (!$apiKey) {
throw new BadCredentialsException();
// or to just skip api key authentication
// return null;
}
return new PreAuthenticatedToken(
'anon.',
$apiKey,
$providerKey
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
if (!$userProvider instanceof ApiKeyUserProvider) {
throw new \InvalidArgumentException(
sprintf(
'The user provider must be an instance of ApiKeyUserProvider (%s was given).',
get_class($userProvider)
)
);
}
$apiKey = $token->getCredentials();
$username = $userProvider->getUsernameForApiKey($apiKey);
if (!$username) {
// CAUTION: this message will be returned to the client
// (so don't put any un-trusted messages / error strings here)
throw new BadCredentialsException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
$user = $userProvider->loadUserByAuthToken($apiKey);
if (!isset($user)) {
throw new BadCredentialsException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
// TODO: HERE, THE $user->getSaltedPasswordHash() RETURNS THE CORRECT VALUE!
return new PreAuthenticatedToken(
$user, // TODO: with "new User()" instead, it works!
$apiKey,
$providerKey,
$user->getRoles()
);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new Response(
// this contains information about *why* authentication failed
// use it, or return your own message
strtr($exception->getMessageKey(), $exception->getMessageData()),
401
);
}
}
Security/ApiKeyUserProvider.php
namespace App\Security;
use App\Entity\AuthToken;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
class ApiKeyUserProvider implements UserProviderInterface
{
/**
* #var EntityManagerInterface
*/
private $em;
/**
* ApiKeyUserProvider constructor.
* #param EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getUsernameForApiKey($apiKey)
{
return $apiKey;
}
public function loadUserByUsername($username)
{
// TODO: Implement loadUserByUsername() method.
}
/**
* Auth token is used as username
*
* #param string $authToken
* #return null|UserInterface
*/
public function loadUserByAuthToken($authToken): ?UserInterface
{
if (!isset($authToken)) {
return null;
}
$token = $this->em
->getRepository(AuthToken::class)
->findOneBy(['id' => AuthToken::hex2dec($authToken)]);
if (!isset($token)) {
return null;
}
return $token->getUser();
}
public
function refreshUser(UserInterface $user)
{
// this is used for storing authentication in the session
// but in this example, the token is sent in each request,
// so authentication can be stateless. Throwing this exception
// is proper to make things stateless
throw new UnsupportedUserException();
}
public
function supportsClass($class)
{
return User::class === $class;
}
}
Entity/User.php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface, EquatableInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* This needs to be nullable because the email includes the id of newly created users, which can only be obtained after inserting the new record.
* #ORM\Column(type="string", length=255, nullable=true, unique=true)
* #Assert\Length(max=255)
* #Assert\NotBlank()
*/
private $email;
/**
* Set null to disable login
* #ORM\Column(type="string", length=255, nullable=true)
* #Assert\Length(max=255)
* #Assert\NotBlank()
*/
private $saltedPasswordHash;
/**
* #ORM\Column(type="datetime")
*/
private $created;
// ...
/**
* #ORM\OneToMany(targetEntity="App\Entity\AuthToken", mappedBy="user", fetch="LAZY")
*/
private $authTokens;
/**
* #ORM\Column(type="string", length=5)
*/
private $role;
// ...
public function __construct()
{
$this->role = 'user';
$this->saltedPasswordHash = null;
}
public function getId()
{
return $this->id;
}
public function getSaltedPasswordHash(): ?string
{
return $this->saltedPasswordHash;
}
public function setSaltedPasswordHash(?string $saltedPasswordHash): self
{
$this->saltedPasswordHash = $saltedPasswordHash;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(?string $email): self
{
$this->email = $email;
return $this;
}
public function getCreated(): ?\DateTimeInterface
{
return $this->created;
}
public function setCreated(\DateTimeInterface $created): self
{
$this->created = $created;
return $this;
}
/**
* #return ArrayCollection
*/
public function getAuthTokens()
{
return $this->authTokens;
}
/**
* #param ArrayCollection $authTokens
* #return User
*/
public function setAuthTokens(ArrayCollection $authTokens): User
{
$this->authTokens = $authTokens;
return $this;
}
/**
* #param AuthToken $authToken
* #return User
*/
public function addAuthToken(AuthToken $authToken): User
{
$this->authTokens->add($authToken);
return $this;
}
/**
* #param AuthToken $authToken
* #return User
*/
public function removeAuthToken(AuthToken $authToken): User
{
$this->authTokens->removeElement($authToken);
return $this;
}
// ...
/**
* Returns the password used to authenticate the user.
*
* This should be the encoded password. On authentication, a plain-text
* password will be salted, encoded, and then compared to this value.
*
* #return string The password
*/
public function getPassword()
{
return $this->getSaltedPasswordHash();
}
/**
* Returns the salt that was originally used to encode the password.
*
* This can return null if the password was not encoded using a salt.
*
* #return string|null The salt
*/
public function getSalt()
{
// TODO: Implement getSalt() method.
}
/**
* Returns the username used to authenticate the user.
*
* #return string The username
*/
public function getUsername()
{
return $this->getEmail();
}
/**
* Removes sensitive data from the user.
*
* This is important if, at any given point, sensitive information like
* the plain-text password is stored on this object.
*/
public function eraseCredentials()
{
$this->setSaltedPasswordHash('');
}
/**
* #return mixed
*/
public function getRole()
{
return $this->role;
}
/**
* #param mixed $role
* #return User
*/
public function setRole($role): User
{
$this->role = $role;
return $this;
}
/**
* #return string
*/
public function __toString()
{
return "User " . $this->email;
}
/**
* The equality comparison should neither be done by referential equality
* nor by comparing identities (i.e. getId() === getId()).
*
* However, you do not need to compare every attribute, but only those that
* are relevant for assessing whether re-authentication is required.
*
* Also implementation should consider that $user instance may implement
* the extended user interface `AdvancedUserInterface`.
*
* https://stackoverflow.com/a/39884792/6144818
*
* #param UserInterface $user
* #return bool
*/
public function isEqualTo(UserInterface $user)
{
return (
$this->getUsername() == $user->getUsername()
) && (
$this->getRoles() == $user->getRoles()
);
}
// ...
}
Entitiy/AuthToken.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\AuthTokenRepository")
*/
class AuthToken
{
/**
* #ORM\Id()
* #ORM\Column(type="decimal", precision=32, scale=0, options={"unsigned": true})
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="authTokens")
*/
private $user;
/**
* #ORM\Column(type="datetime")
*/
private $added;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $lastSeen;
/**
* #ORM\Column(type="string", length=12, nullable=true)
*/
private $apiVersion;
/**
* #return string
*/
public function getId(): string
{
return $this->id;
}
/**
* #return string
*/
public function getHexId(): string
{
return $this->dec2hex($this->id);
}
/**
* #param mixed $id
*/
public function setId($id): void
{
$this->id = $id;
}
/**
* #param mixed $id
* #throws \Exception
*/
public function generateId(): void
{
$length = 32;
$str = "";
$characters = range('0', '9');
$max = count($characters) - 1;
for ($i = 0; $i < $length; $i++) {
$rand = random_int(0, $max);
$str .= $characters[$rand];
}
$this->id = $str;
}
/**
* #return mixed
*/
public function getUser()
{
return $this->user;
}
/**
* #param mixed $user
*/
public function setUser($user): void
{
$this->user = $user;
}
/**
* #return mixed
*/
public function getAdded()
{
return $this->added;
}
/**
* #param mixed $added
*/
public function setAdded($added): void
{
$this->added = $added;
}
/**
* #return mixed
*/
public function getLastSeen()
{
return $this->lastSeen;
}
/**
* #param mixed $lastSeen
*/
public function setLastSeen($lastSeen): void
{
$this->lastSeen = $lastSeen;
}
public function getApiVersion(): ?string
{
return $this->apiVersion;
}
public function setApiVersion(string $apiVersion): self
{
$this->apiVersion = $apiVersion;
return $this;
}
public static function dec2hex(string $dec): string
{
$hex = '';
do {
$last = bcmod($dec, 16);
$hex = dechex($last) . $hex;
$dec = bcdiv(bcsub($dec, $last), 16);
} while ($dec > 0);
return $hex;
}
public static function hex2dec($hex)
{
$dec = '0';
$len = strlen($hex);
for ($i = 1; $i <= $len; $i++)
$dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
return $dec;
}
/**
* #return string
*/
public function __toString()
{
return "AuthToken " . $this->id . " (" . $this->user . ")";
}
}
This function of your Entity/User.php is the reason of your behavior:
/**
* Removes sensitive data from the user.
*
* This is important if, at any given point, sensitive information like
* the plain-text password is stored on this object.
*/
public function eraseCredentials()
{
$this->setSaltedPasswordHash('');
}
When authentication comes in play on Symfony, after the authentication, the AuthenticationProviderManager would call that eraseCredentials function, so it doesn't leak sensible information, or even worse, the sensible information does not end up in your session.
Just try to comment the setter in that function and you should have what you expect.

Symfony 4 - Define custom user roles from DB

I am learning Symfony 4, and I am lost with user roles and I don't know where to start.
Because it will be an intranet like website Users will not register themselves, the admin will register them and set the role. The admin can also create new roles dynamically.
I would like to store user roles in database. I have 2 entities User and Role. (Relations are not defined yet, that´s normal)
Here is my User entity :
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity(fields="email", message="Email already taken")
* #UniqueEntity(fields="username", message="Username already taken")
*/
class User implements UserInterface, \Serializable
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer", unique=true)
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $firstname;
/**
* #ORM\Column(type="string", length=255)
*/
private $lastname;
/**
* #ORM\Column(type="string", length=255)
*/
private $email;
/**
* #ORM\Column(type="date")
*/
private $birthdate;
/**
* #ORM\Column(type="integer", nullable=true)
*/
private $roleId;
/**
* #ORM\Column(type="string", length=255, nullable=false)
*/
private $username;
/**
* #Assert\NotBlank()
* #Assert\Length(max=4096)
*/
private $plainPassword;
/**
* #ORM\Column(type="string", length=255, nullable=false)
*/
private $password;
/**
* #ORM\Column(name="is_active", type="boolean", nullable=true)
*/
private $isActive;
// GETTERS
public function getId()
{
return $this->id;
}
public function getFirstname()
{
return $this->firstname;
}
public function getLastname()
{
return $this->lastname;
}
public function getBirthdate()
{
return $this->birthdate;
}
public function getEmail()
{
return $this->email;
}
public function getRoleId()
{
return $this->roleId;
}
public function getRoles()
{
return array('ROLE_USER');
}
public function getUsername()
{
return $this->username;
}
public function getPlainPassword()
{
return $this->plainPassword;
}
public function getPassword()
{
return $this->password;
}
public function getSalt()
{
return null;
}
// SETTERS
public function setFirstname($firstname)
{
$this->firstname = $firstname;
}
public function setLastname($lastname)
{
$this->lastname = $lastname;
}
public function setEmail($email)
{
$this->email = $email;
}
public function setBirthdate($birthdate)
{
$this->birthdate = $birthdate;
}
public function setRoleId($roleId)
{
$this->roleId = $roleId;
}
public function setUsername($username)
{
$this->username = $username;
}
public function setPlainPassword($password)
{
$this->plainPassword = $password;
}
public function setPassword($password)
{
$this->password = $password;
}
// FUNCTIONS
public function serialize()
{
return serialize(array(
$this->id,
$this->username,
$this->password,
// see section on salt below
// $this->salt,
));
}
/** #see \Serializable::unserialize() */
public function unserialize($serialized)
{
list (
$this->id,
$this->username,
$this->password,
// see section on salt below
// $this->salt
) = unserialize($serialized);
}
public function eraseCredentials()
{
}
}
Here is my Role entity :
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\RoleRepository")
*/
class Role
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer", unique=true)
*/
private $id;
/**
* #ORM\Column(type="string", length=255, nullable=false)
*/
private $type;
// GETTERS
public function getId()
{
return $this->id;
}
public function getType()
{
return $this->type;
}
// SETTERS
public function setType($type): void
{
$this->type = $type;
}
}
Is it a good practice to do so ??
On the Symfony documentation, we can read everywhere the "macros" ROLE_USER, ROLE_ADMIN... Where are they defined ? Can we customize this ?
Symfony used to support role entities with a RoleInterface, much like with your user implementing the UserInterface. It was decided that this is unnecessary as the roles-table will usually only contain 2 fields (id, name) which means we might just as well store the roles directly in the users table.
So, if you want to follow Symfony best practices you would just have your roles in the user, for example like this:
class User implements UserInterface
{
/** #Column(type="json") */
private $roles = [];
public function getRoles(): array
{
return array_unique(array_merge(['ROLE_USER'], $this->roles));
}
public function setRoles(array $roles)
{
$this->roles = $roles;
}
public function resetRoles()
{
$this->roles = [];
}
}
If you don't want to store the roles as JSON-encoded string you can also use #Column(type="array") */ or write a custom DBAL type, e.g some kind of EnumType. You might also want to secure the setter against invalid data or add validation.
Regarding the second question about the roles used in the documentation: Symfony has some predefined roles and pseudo-roles for certain features:
IS_AUTHENTICATED_ANONYMOUSLY
IS_AUTHENTICATED_REMEMBERED
IS_AUTHENTICATED_FULLY
ROLE_ALLOWED_TO_SWITCH (for user switching)
The other roles like ROLE_USER and ROLE_ADMIN are purely exemplary and you may use them as you see fit. You do not even have to start your own roles with a ROLE_ (although some security features rely on this convention and things will not work as expected for those roles unless you do some manual changes).
Recommended Reading from current Symfony Documentation (version 4 at the time of this answer):
https://symfony.com/doc/current/security.html#hierarchical-roles
Symfony recommends defining role inheritance, please see if it helps; from the link just above,
"
Instead of giving many roles to each user, you can define role inheritance rules by creating a role hierarchy:
# config/packages/security.yaml
security:
# ...
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
Users with the ROLE_ADMIN role will also have the ROLE_USER role. And users with ROLE_SUPER_ADMIN, will automatically have ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH and ROLE_USER (inherited from ROLE_ADMIN).
For role hierarchy to work, do not try to call $user->getRoles() manually. For example, in a controller extending from the base controller:
// BAD - $user->getRoles() will not know about the role hierarchy
$hasAccess = in_array('ROLE_ADMIN', $user->getRoles());
// GOOD - use of the normal security methods
$hasAccess = $this->isGranted('ROLE_ADMIN');
$this->denyAccessUnlessGranted('ROLE_ADMIN');
" end of copying
I think storing the role as a string is enough if inheritance fits your needs.

Resources