I'm trying to grant roles to a user according to their LDAP dn when they log in through the LDAP.
To do that, I'd like to override the loadUser method from Symfony\Component\Security\Core\User\LdapUserProvider, but I don't really know how to proceed as, if I understand correctly (I'm quite new at using Symfony :p), it's not a service, but part of one?
So, is there a way to override that method easily, or do I need to redefine the whole Ldap service?
What I've tried is:
// app/config/services.yml
[...]
Symfony\Component\Ldap\Ldap:
arguments: ['#Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
Symfony\Component\Security\Core\User\LdapUserProvider:
class: AppBundle\Services\LdapUserProvider
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
arguments:
- host: '%ldap_host%'
port: '%ldap_port%'
// src/Services/LdapUserProvider.php
namespace AppBundle\Services;
use Symfony\Component\Ldap\Entry;
class LdapUserProvider extends \Symfony\Component\Security\Core\User\LdapUserProvider {
protected function loadUser($username, Entry $entry)
{
$password = null;
if (null !== $this->passwordAttribute) {
$password = $this->getAttributeValue($entry, $this->passwordAttribute);
}
return new User($username, $password, array('ROLE_TEST'));
}
}
But of course it doesn't work and I don't get the ROLE_TEST role.
Thanks by advance!
You can use this tutorial:
https://medium.com/#devstan/extended-ldap-with-symfony-3-30be6f1a36b1
and you should create a LdapUser class that implements UserInterface
1.LDAP user provider
<?php
namespace YourAppBundle\Security\User;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Security\Core\User\LdapUserProvider as BaseLdapUserProvider;
class LdapUserProvider extends BaseLdapUserProvider
{
/**
* {#inheritdoc}
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof LdapUser) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
return new LdapUser($user->getUsername(), null, $user->getRoles(), $user->getLdapEntry());
}
/**
* {#inheritdoc}
*/
public function supportsClass($class)
{
return $class === 'YourAppBundle\Security\User\LdapUser';
}
/**
* {#inheritdoc}
*/
protected function loadUser($username, Entry $entry)
{
$user = parent::loadUser($username, $entry);
return new LdapUser($username, $user->getPassword(), $user->getRoles(), $entry);
}
}
2.LDAP user
<?php
namespace YourAppBundle\Security\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Ldap\Entry;
class LdapUser implements UserInterface
{
const LDAP_KEY_DISPLAY_NAME = 'displayName';
const LDAP_KEY_MAIL = 'mail';
protected $username;
protected $password;
protected $roles;
protected $ldapEntry;
protected $displayName;
protected $eMail;
public function __construct($username, $password, array $roles, Entry $ldapEntry)
{
if ('' === $username || null === $username) {
throw new \InvalidArgumentException('The username cannot be empty.');
}
$this->username = $username;
$this->password = $password;
$this->roles = $roles;
$this->ldapEntry = $ldapEntry;
$this->displayName = $this->extractSingleValueByKeyFromEntry(
$ldapEntry,
self::LDAP_KEY_DISPLAY_NAME,
$username
);
$this->eMail = $this->extractSingleValueByKeyFromEntry($ldapEntry, self::LDAP_KEY_MAIL);
}
public function __toString()
{
return (string) $this->getUsername();
}
public function getDisplayName()
{
return $this->displayName;
}
/**
* #return Entry
*/
public function getLdapEntry()
{
return $this->ldapEntry;
}
/**
* {#inheritdoc}
*/
public function getRoles()
{
return $this->roles;
}
/**
* {#inheritdoc}
*/
public function getPassword()
{
return $this->password;
}
/**
* {#inheritdoc}
*/
public function getSalt()
{
}
/**
* {#inheritdoc}
*/
public function getUsername()
{
return $this->username;
}
/**
* {#inheritdoc}
*/
public function eraseCredentials()
{
}
/**
* Extracts single value from entry's array value by key.
*
* #param Entry $entry Ldap entry
* #param string $key Key
* #param null|string $defaultValue Default value
*
* #return string|null
*/
protected function extractSingleValueByKeyFromEntry(Entry $entry, $key, $defaultValue = null)
{
$value = $this->extractFromLdapEntry($entry, $key, $defaultValue);
return is_array($value) && isset($value[0]) ? $value[0] : $defaultValue;
}
/**
* Extracts value from entry by key.
*
* #param Entry $entry Ldap entry
* #param string $key Key
* #param mixed $defaultValue Default value
*
* #return array|mixed
*/
protected function extractFromLdapEntry(Entry $entry, $key, $defaultValue = null)
{
if (!$entry->hasAttribute($key)) {
return $defaultValue;
}
return $entry->getAttribute($key);
}
}
3.Services.yml
we need to define our newly created ldap user provider service
services:
...
app.ldap:
class: Symfony\Component\Ldap\Ldap
factory: ['Symfony\Component\Ldap\Ldap', 'create']
arguments:
- 'ext_ldap'
- host: '%ldap_host%'
app.ext_ldap_user_provider:
class: YourAppBundle\Security\User\LdapUserProvider
arguments:
- '#app.ldap' # LDAP component instance
- '%ldap_base_dn%' # Base dn
- '%ldap_search_dn%' # Search dn
- '%ldap_search_password%' # Search user password
- ['ROLE_SUPER_ADMIN'] # Roles
- '%ldap_uid_key%' # LDAP uid key
- '%ldap_filter%' # filter
4.Security.yml
Here is just an example usage of our provider — the “/ui” will be secured with LDAP.
security:
encoders:
...
YourAppBundle\Security\User\LdapUser:
algorithm: bcrypt
cost: 12
providers:
...
ldap_users:
id: app.ext_ldap_user_provider
...
firewalls:
frontend:
anonymous: ~
provider: ldap_users
form_login_ldap:
service: app.ldap
dn_string: '%ldap_dn_string%'
login_path: front-login
check_path: front-login
default_target_path: /ui
logout:
path: front-logout
target: front-login
access_control:
...
- { path: ^/ui/logout, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/ui/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/ui, roles: IS_AUTHENTICATED_FULLY }
Related
Versions I Used:
using (symfony 5.2.6) with (api-platform 2.7.0) alongside (php 8.0.3) and (postgres 13)
Description
used maker bundle to generate a User Entity and configured the security.yaml to encode passwords already tried auto and bcrypt or even argon2i non of them seems to work and hash the passwords
Possible Solution
as the symfony documentation describes it should automatically encode passwords if we are using security bundle and implementing UserInterface but this one seems like a bug cause i have tried many things nothing works . maybe using UserPasswordEncoderInterface but this one should not be used when User class is implementing UserInterface anyway i hope someone check this and tell me i'm wrong otherwise seems like a bug.
security.yaml:
security:
encoders:
App\Entity\User:
algorithm: bcrypt
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
provider: app_user_provider
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
# or require ROLE_ADMIN or IS_AUTHENTICATED_FULLY for /admin*
# - { path: '^/admin', roles: [IS_AUTHENTICATED_FULLY, ROLE_ADMIN] }
# the 'path' value can be any valid regular expression
# (this one will match URLs like /api/post/7298 and /api/comment/528491)
# - { path: ^/api/(post|comment)/\d+$, roles: ROLE_USER }
User.php:
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use App\Entity\Trait\HasDateTime;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ApiResource
* #ORM\Entity(repositoryClass=UserRepository::class)
* #ORM\Table(name="`user`")
* #ORM\HasLifecycleCallbacks
*/
class User implements UserInterface
{
use HasDateTime;
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
#[Assert\NotBlank]
#[Assert\Email]
private $email;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
#[Assert\NotBlank]
private $username;
/**
* #ORM\Column(type="json")
*/
private $roles = [];
/**
* #var string The hashed password
* #ORM\Column(type="string")
*/
#[Assert\NotBlank]
private $password;
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): string
{
return (string) $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* #see UserInterface
*/
public function getUsername(): string
{
return (string) $this->username;
}
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
/**
* #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 UserInterface
*/
public function getPassword(): string
{
return (string) $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;
}
}
tried to save data in the database each time it saved passwords as plaintext.
also tried to remove the trait class thought it might conflict some how which seems strange(i know) (but i just gave it a try) it was not hashing passwords even after removing the trait. what am i doing wrong or missing here?
thanks in advance
here is the solution:
i had to create a class that implements DataPersister then in there we have to encode the password . here is the code:
<?php
namespace App\DataPersister;
use App\Entity\User;
use ApiPlatform\Core\DataPersister;
use Doctrine\ORM\EntityManagerInterface;
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class UserDataPersister implements DataPersisterInterface
{
public function __construct(private EntityManagerInterface $entityManager, private UserPasswordEncoderInterface $userPasswordEncoderInterface)
{
}
/**
* Is the data supported by the persister?
*/
public function supports($data): bool
{
return $data instanceof User;
}
/**
* #param User $data
* #return object|void Void will not be supported in API Platform 3, an object should always be returned
*/
public function persist($data)
{
if ($data->getPassword()) {
$data->setPassword(
$this->userPasswordEncoderInterface->encodePassword($data, $data->getPassword())
);
}
$this->entityManager->persist($data);
$this->entityManager->flush();
}
/**
* Removes the data.
*/
public function remove($data)
{
$this->entityManager->remove($data);
$this->entityManager->flush();
}
}
I'm using a custom authenticator and a custom user provider in Symfony 5.0.10
Some of my users have complained that they can't login anymore : in fact in some cases, the login will be sucessful (onAuthenticationSuccess is called) but the user will still be anonymous. This causes a direct redirection to login page.
This is solved by clearing the cookies (PHPSESSID) or by using a private navigation window. I can't explain how thats comes into the login logic of an anonymous user.
If you guys can find the issue that would really help me, i've been spending a few all nighters on this and can't figure it out.
Here is my code :
security.yaml
security:
encoders:
App\Security\User:
algorithm: none
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_galette_user_provider:
id: App\Security\GaletteUserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: lazy
provider: app_galette_user_provider
logout:
path: app_logout
guard:
authenticators:
- App\Security\AppCustomAuthenticator
# where to redirect after logout
# target: app_any_route
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
Custom Authenticator (AppCustomAuthenticator.php)
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class AppCustomAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{
private $urlGenerator;
private $csrfTokenManager;
private $passwordEncoder;
public function __construct(UrlGeneratorInterface $urlGenerator, UserPasswordEncoderInterface $passwordEncoder)
{
$this->urlGenerator = $urlGenerator;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request)
{
return 'app_login' === $request->attributes->get('_route') && $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'username' => $request->request->get('email'),
'password' => $request->request->get('password'),
//'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['username']
);
return $credentials;
}
public function supportsRememberMe()
{
return false;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
// Load / create our user however you need.
// You can do this by calling the user provider, or with custom logic here.
try {
$user = $userProvider->loadUserByUsername($credentials['username']);
} catch (UsernameNotFoundException $e) {
throw new CustomUserMessageAuthenticationException("Erreur lors de la connexion : veuillez vérifier vos identifiants et l'état de votre cotisation.");
}
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Email could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
if ($credentials['password'] === $user->getPassword()) {
return true;
}
return false;
}
/**
* Used to upgrade (rehash) the user's password automatically over time.
*/
public function getPassword($credentials): ?string
{
return $credentials['password'];
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return new RedirectResponse($this->urlGenerator->generate('index'));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate("app_login");
}
}
And my User.php entity :
<?php
namespace App\Security;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class User implements UserInterface, EquatableInterface
{
private $id;
private $email;
private $roles;
private $password;
private $nom;
private $prenom;
private $adresse;
private $adresse2;
private $cp;
private $ville;
private $pays;
private $tel;
private $gsm;
private $salt;
private $username;
public function isEqualTo(UserInterface $user)
{
if ($this->getUsername() !== $user->getUsername()) {
return false;
}
return true;
}
/**
* #return int
*/
public function getId(): int
{
return $this->id;
}
/**
* #param int $id
*/
public function setId($id): void
{
$this->id = $id;
}
/**
* #return string
*/
public function getNom(): string
{
return $this->nom;
}
/**
* #param string $nom
*/
public function setNom($nom): void
{
$this->nom = $nom;
}
/**
* #return string
*/
public function getPrenom(): string
{
return $this->prenom;
}
/**
* #param string $prenom
*/
public function setPrenom($prenom): void
{
$this->prenom = $prenom;
}
/**
* #return string
*/
public function getAdresse(): string
{
return $this->adresse;
}
/**
* #param string $adresse
*/
public function setAdresse($adresse): void
{
$this->adresse = $adresse;
}
/**
* #return string
*/
public function getAdresse2(): ?string
{
return $this->adresse2;
}
/**
* #param string $adresse2
*/
public function setAdresse2($adresse2): void
{
$this->adresse2 = $adresse2;
}
/**
* #return string
*/
public function getCp(): ?string
{
return $this->cp;
}
/**
* #param string $cp
*/
public function setCp($cp): void
{
$this->cp = $cp;
}
/**
* #return string
*/
public function getVille(): ?string
{
return $this->ville;
}
/**
* #param string $ville
*/
public function setVille($ville): void
{
$this->ville = $ville;
}
/**
* #return string
*/
public function getPays(): ?string
{
return $this->pays;
}
/**
* #param string $pays
*/
public function setPays($pays): void
{
$this->pays = $pays;
}
/**
* #return string
*/
public function getTel(): ?string
{
return $this->tel;
}
/**
* #param string $tel
*/
public function setTel($tel): void
{
$this->tel = $tel;
}
/**
* #return string
*/
public function getGsm(): ?string
{
return $this->gsm;
}
/**
* #param string $gsm
*/
public function setGsm($gsm): void
{
$this->gsm = $gsm;
}
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 getUsername(): string
{
return $this->username;
}
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
/**
* #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 UserInterface
*/
public function getPassword(): string
{
return (string) $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* #see UserInterface
*/
public function getSalt()
{
return;
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
}
'''
-> I'm not including my custom user provider as i know it to work (it correctly returns the user)
-> My user passwords are indeed "in clear", this is a very specific scenario which poses no security threat
This may be caused by symfony session fixation protection.
It is enabled by default and should refresh session id after user authentication. More info in symfony docs
Check if the PHPSESSID cookie refreshes after EVERY request.
If it does, then your authenticator triggers this method refreshing session id on each user request.
Which leads to the following: if the user makes second request before they receive the response from the previous, their session id becomes invalid, and they become unauthenticated.
You can of course disable this protection in your security config:
security:
session_fixation_strategy: none
but better is to fix the problem and do not create a vulnerability in your system.
I'm pretty new to Symfony in general, I mostly used it because I needed to do something secure very fast, and also to discover Symfony 4.
I'm trying to make a secure connexion with the Security recipe but I'm facing two major problems (probably related) and a small one.
First, I tried to define the salt as nullable but it's still NOT NULL in db. Here's my definition of the column :
/**
* #ORM\Column(name="salt", type="string", nullable=true)
*/
private $salt;
So now the big problems : Passwords I add are not hashed and trying to connect returns error 500
I tried to follow the documentation and here are :
My Entity
use Doctrine\ORM\Mapping as ORM;
use PhpParser\Node\Scalar\String_;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Table(name="app_user")
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface, \Serializable
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=25, unique=true)
*/
private $username;
/**
* #ORM\Column(type="string", length=255)
*/
private $password;
/**
* #ORM\Column(type="string", length=254, unique=true, nullable=true)
*/
private $email;
/**
* #ORM\Column(name="is_active", type="boolean")
*/
private $isActive;
/**
* #ORM\Column(name="salt", type="string", nullable=true)
*/
private $salt;
/**
* #ORM\Column(name="alias", type="string")
*/
private $alias;
/**
* #return mixed
*/
public function getAlias()
{
return $this->alias;
}
/**
* #param mixed $alias
*/
public function setAlias($alias): void
{
$this->alias = $alias;
}
public function __construct()
{
$this->isActive = true;
// may not be needed, see section on salt below
// $this->salt = md5(uniqid('', true));
}
public function getUsername()
{
return $this->username;
}
public function getSalt() :String
{
// you *may* need a real salt depending on your encoder
// see section on salt below
return $this->salt;
}
public function getPassword()
{
return $this->password;
}
public function getRoles()
{
return array('ROLE_USER');
}
public function eraseCredentials()
{
}
/** #see \Serializable::serialize() */
public function serialize()
{
return serialize([
$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, ['allowed_classes' => false]);
}
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #param mixed $id
*/
public function setId($id): void
{
$this->id = $id;
}
/**
* #return mixed
*/
public function getEmail()
{
return $this->email;
}
/**
* #param mixed $email
*/
public function setEmail($email): void
{
$this->email = $email;
}
/**
* #return mixed
*/
public function getisActive()
{
return $this->isActive;
}
/**
* #param mixed $isActive
*/
public function setIsActive($isActive): void
{
$this->isActive = $isActive;
}
/**
* #param mixed $username
*/
public function setUsername($username): void
{
$this->username = $username;
}
/**
* #param mixed $password
*/
public function setPassword($password): void
{
$this->password = $password;
}
/**
* #param mixed $salt
*/
public function setSalt($salt): void
{
$this->salt = $salt;
}
}
My Controllers
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends Controller
{
/**
* #Route("/login", name="login")
*/
public function login(Request $request, AuthenticationUtils $authenticationUtils)
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('security/login.html.twig', array(
'last_username' => $lastUsername,
'error' => $error,
));
}
}
and
use App\Entity\User;
use App\Form\UserType;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
/**
* #Route("/user")
*/
class UserController extends Controller
{
/**
* #Route("/", name="user_index", methods="GET")
*/
public function index(UserRepository $userRepository): Response
{
return $this->render('user/index.html.twig', ['users' => $userRepository->findAll()]);
}
/**
* #Route("/new", name="user_new", methods="GET|POST")
*/
public function new(Request $request): Response
{
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
return $this->redirectToRoute('user_index');
}
return $this->render('user/new.html.twig', [
'user' => $user,
'form' => $form->createView(),
]);
}
/**
* #Route("/{id}", name="user_show", methods="GET")
*/
public function show(User $user): Response
{
return $this->render('user/show.html.twig', ['user' => $user]);
}
/**
* #Route("/{id}/edit", name="user_edit", methods="GET|POST")
*/
public function edit(Request $request, User $user): Response
{
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('user_edit', ['id' => $user->getId()]);
}
return $this->render('user/edit.html.twig', [
'user' => $user,
'form' => $form->createView(),
]);
}
/**
* #Route("/{id}", name="user_delete", methods="DELETE")
*/
public function delete(Request $request, User $user): Response
{
if ($this->isCsrfTokenValid('delete'.$user->getId(), $request->request->get('_token'))) {
$em = $this->getDoctrine()->getManager();
$em->remove($user);
$em->flush();
}
return $this->redirectToRoute('user_index');
}
public function register(User $user, UserPasswordEncoderInterface $encoder)
{
$plainPassword = $user->getPassword();
$encoded = $encoder->encodePassword($user, $plainPassword);
$user->setPassword($encoded);
}
}
and my security.yaml
security:
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
db_provider:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
provider: db_provider
form_login:
login_path: login
check_path: login
logout:
path: /logout
target: /homepage
pattern: ^/admin
http_basic: ~
encoders:
App\Entity\User:
algorithm: argon2i
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
I tried to add this after checking if for isSubmited and isValid in my UserController::new()
$plainPassword = $user->getPassword;
$encoded = $encoder->encodePassword($user, $plainPassword);
$user->setPassword($encoded);
But I had an error Saying that the UserPasswordEncoderInterface $encoder I passed as method argument wasn't injected when loading the form. Still I'm not sure it would be a good solution to make it work as I would have to duplicate that logic in the UserController::edit(), which does not look like Symfony-like code.
(the error :)
"Controller "App\Controller\UserController::new()" requires that you provide a value for the "$encoder" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one."
I also tried to copy/paste (that how desperate I am...) the code in my UserController and then the SecurityController but this didn't work either
public function register(UserPasswordEncoderInterface $encoder)
{
// whatever *your* User object is
$user = new App\Entity\User();
$plainPassword = 'ryanpass';
$encoded = $encoder->encodePassword($user, $plainPassword);
$user->setPassword($encoded);
}
I'm getting this as log from the server :
"No encoder has been configured for account "App\Entity\User"."
I also tried to insert directly in my db some values, but trying to connect gave me a "Access Denied" message when entering the right password, which I think is another problem...
I really don't get where I'm wrong and I couldn't find people asking about this. I'd be sincerely grateful if you could help me.
Note :
The UserController routes start with /user and is completely public as I need a user to access secured admin panel.
EDIT
I'm using MySQL 5.7 and PHP 7.2 if that can be related
Since you're using Argon2i as the encoder algorithm for your entity, your $salt becomes obsolete:
Do you need to use a Salt property?
If you use bcrypt or argon2i, no. Otherwise, yes. All passwords must be hashed with a salt, but bcrypt and argon2i do this internally [...] the getSalt() method in User can just return null (it's not used). [...]
-How to Load Security Users from the Database (the Entity Provider)
Try removing the $salt property and the setter method, and let your getSalt() return null. Persist the user without encoding operations and check the persisted password.
While this can be seen as a dirty hack, it seems to be a good practice...
I finnally found a solution thanks to #LeonWillens. Actually removing the salt property and setters made me discover that the security recipe come without the validator. So I ran composer require doctrine form security validator. I added a plainText field in my Entity which is not a column
/**
* #Assert\NotBlank()
* #Assert\Length(max=4096)
*/
private $plainPassword;
With that, I could add this logic in UserController::new()
/**
* #Route("/new", name="user_new", methods="GET|POST")
*/
public function new(Request $request, UserPasswordEncoderInterface $passwordEncoder): Response
{
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$password = $passwordEncoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
return $this->redirectToRoute('user_index');
}
return $this->render('user/new.html.twig', [
'user' => $user,
'form' => $form->createView(),
]);
}
I change the encoders in my security.yaml
encoders:
Symfony\Component\Security\Core\User\User: plaintext
App\Entity\User:
algorithm: argon2i
And now adding a user work perfectly. I still have problems with connexion, but no such thing as an Exception thrown
I'm trying to build a login/register controller for users but i get Underfined class constant 'ROLE_DEFAULT' when I register new user.
In my UserController, I have this function to register new user:
//src/Controller/UserController
public function registerAction(Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
// 1) build the form
$user = new User();
$form = $this->createForm(UserType::class, $user);
// 2) handle the submit (will only happen on POST)
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// 3) Encode the password (you could also do this via Doctrine listener)
$password = $passwordEncoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
$apiKey = $passwordEncoder->encodePassword($user, rand(1,9999));
$user->setApiKey($apiKey);
$user->setEnabled(1);
// $user->setSuperadmin(true);
$user->addRole("ROLE_DEFAULT");
// 4) save the User!
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
dump($user);
exit;
}
return $this->render(
'user/register.html.twig',
array('form' => $form->createView())
);
}
The error appear when "$user->addRole("ROLE_DEFAULT");" and this is the screenshot:
//src/Entity/User:
namespace App\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="user")
*/
class User implements UserInterface
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", unique=true)
*/
private $username;
/**
* #ORM\Column(name="lastname", type="string", nullable=true)
*/
protected $lastname;
/**
* #ORM\Column(type="string", unique=true)
*/
private $apiKey;
/**
* #ORM\Column(name="enabled", type="boolean")
*/
protected $enabled;
/**
* #ORM\Column(name="email", type="string", nullable=false)
*/
protected $email;
/**
* #ORM\Column(name="SUPERADMIN", type="boolean", nullable=true)
*/
protected $superadmin;
/**
* The salt to use for hashing
*
* #ORM\Column(name="salt", type="string", nullable=true)
*/
protected $salt;
/**
* Encrypted password. Must be persisted.
*
* #ORM\Column(name="password", type="string", nullable=false)
*/
protected $password;
/**
* Encrypted password. Must be persisted.
*
* #ORM\Column(name="plainPassword", type="string", nullable=false)
*/
protected $plainPassword;
/**
* #var \DateTime
*/
protected $lastLogin;
/**
* Random string sent to the user email address in order to verify it
*
* #var string
*/
protected $confirmationToken;
/**
* #var \DateTime
*/
protected $passwordRequestedAt;
public function __construct()
{
$this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36);
$this->enabled = false;
// $this->locked = false;
$this->roles = array();
}
public function __toString()
{
return (string) $this->getUsername();
}
public function getId()
{
return $this->id;
}
// USERNAME//
//===========================================================
public function setUsername($username)
{
$this->username = $username;
return $this;
}
public function getUsername()
{
return $this->username;
}
// EMAIL//
//===========================================================
public function getEmail()
{
return $this->email;
}
public function setEmail($email)
{
$this->email = $email;
return $this;
}
// API KEY//
//===========================================================
public function getApiKey()
{
return $this->apiKey;
}
public function setApiKey($apiKey)
{
return $this->apiKey = $apiKey;
}
// PASSWORD//
//===========================================================
/**
* Gets the encrypted password.
*
* #return string
*/
public function getPassword()
{
return $this->password;
}
public function setPassword($password)
{
$this->password = $password;
return $this;
}
/**
* Gets the encrypted password.
*
* #return string
*/
public function getPlainPassword()
{
return $this->plainPassword;
}
public function setPlainPassword($password)
{
$this->plainPassword = $password;
}
// SALT//
//===========================================================
public function getSalt()
{
return $this->salt;
}
public function setSalt($salt)
{
return $this->salt = $salt;
}
// LAST LOGIN//
//===========================================================
/**
* Gets the last login time.
*
* #return \DateTime
*/
public function getLastLogin()
{
return $this->lastLogin;
}
public function setLastLogin(\DateTime $time = null)
{
$this->lastLogin = $time;
return $this;
}
// CONFIRMATION TOKEN//
//===========================================================
public function getConfirmationToken()
{
return $this->confirmationToken;
}
public function setConfirmationToken($confirmationToken)
{
$this->confirmationToken = $confirmationToken;
return $this;
}
/**
* Removes sensitive data from the user.
*/
public function eraseCredentials()
{
$this->plainPassword = null;
}
// ROLES//
//===========================================================
// public function getRoles()
// {
// return array('ROLE_USER');
// }
/**
* Returns the user roles
*
* #return array The roles
*/
public function getRoles()
{
$roles = array();
$nameRole = $this->idRole->getNamerole();
$stationCreate = $this->idRole->getStationCreate();
$configurationstationEdit = $this->idRole->getConfigurationstationEdit();
$stationEditor = $this->idRole->getDatastationEdit();
$stationValidate = $this->idRole->getStationValidate();
$newdataCreate = $this->idRole->getNewdataCreate();
$superadmin = $this->superadmin;
if($stationCreate){
$roles = array_merge($roles, array('ROLE_CREATOR'));
}
if($configurationstationEdit){
$roles = array_merge($roles, array('ROLE_CONFIGURATOR'));
}
if($stationEditor){
$roles = array_merge($roles, array('ROLE_EDITOR'));
}
if($stationValidate){
$roles = array_merge($roles, array('ROLE_VALIDATOR'));
}
if($newdataCreate){
$roles = array_merge($roles, array('ROLE_MANAGE'));
}
if($superadmin){
$roles = array_merge($roles, array('ROLE_SUPER_ADMIN'));
}
//echo $nameRole." -- ".$stationCreate." -- ".$configurationstationEdit." -- ".$stationValidate." -- ".$newdataCreate;
//exit;
//dump($this->idRole);
//exit;
// we need to make sure to have at least one role
$roles[] = static::ROLE_DEFAULT;
return array_unique($roles);
}
public function setRoles(array $roles)
{
$this->roles = array();
foreach ($roles as $role) {
$this->addRole($role);
}
return $this;
}
public function removeRole($role)
{
if (false !== $key = array_search(strtoupper($role), $this->roles, true)) {
unset($this->roles[$key]);
$this->roles = array_values($this->roles);
}
return $this;
}
public function hasRole($role)
{
return in_array(strtoupper($role), $this->getRoles(), true);
}
public function addRole($role)
{
$role = strtoupper($role);
if ($role === static::ROLE_DEFAULT) {
return $this;
}
if (!in_array($role, $this->roles, true)) {
$this->roles[] = $role;
}
return $this;
}
// ENABLED//
//===========================================================
public function isEnabled()
{
return $this->enabled;
}
public function setEnabled($boolean)
{
$this->enabled = (Boolean) $boolean;
return $this;
}
// SUPERADMIN//
//===========================================================
public function isSuperAdmin()
{
return $this->hasRole(static::ROLE_SUPER_ADMIN);
}
public function setSuperAdmin($boolean)
{
if (true === $boolean) {
$this->addRole(static::ROLE_SUPER_ADMIN);
} else {
$this->removeRole(static::ROLE_SUPER_ADMIN);
}
return $this;
}
// PASSWORD REQUEST AT//
//===========================================================
/**
* Gets the timestamp that the user requested a password reset.
*
* #return null|\DateTime
*/
public function getPasswordRequestedAt()
{
return $this->passwordRequestedAt;
}
public function isPasswordRequestNonExpired($ttl)
{
return $this->getPasswordRequestedAt() instanceof \DateTime &&
$this->getPasswordRequestedAt()->getTimestamp() + $ttl > time();
}
// EXTRA FUNCTIONS//
//===========================================================
/**
* Serializes the user.
*
* The serialized data have to contain the fields used during check for
* changes and the id.
*
* #return string
*/
public function serialize()
{
return serialize(array(
$this->password,
$this->salt,
$this->username,
$this->enabled,
$this->id,
$this->email,
));
}
/**
* Unserializes the user.
*
* #param string $serialized
*/
public function unserialize($serialized)
{
$data = unserialize($serialized);
// add a few extra elements in the array to ensure that we have enough keys when unserializing
// older data which does not include all properties.
$data = array_merge($data, array_fill(0, 2, null));
list(
$this->password,
$this->salt,
$this->username,
$this->enabled,
$this->id,
$this->email,
) = $data;
}
}
And the security.yml file
//config/packages/security.yml
security:
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
encoders:
App\Entity\User:
algorithm: bcrypt
providers:
in_memory: { memory: ~ }
user:
entity:
class: App\Entity\User
# property: apiKey
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
pattern: ^/
http_basic: ~
provider: user
# activate different ways to authenticate
# http_basic: true
# https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate
# form_login: true
# https://symfony.com/doc/current/security/form_login_setup.html
# form_login:
# login_path: security_login
# check_path: security_login
# csrf_token_generator: security.csrf.token_manager
# default_target_path: userRedirectAction
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/user, roles: ROLE_USER }
I want to know what I'm doing wrong or where can i find a good guide to build a login/register system in SYMFONY 4.
Thank you.
The first thing that I noticed is use of static to (presumably) get constant. If that was the intention, it is not right.
See this comment of PHP constatnt official docs to get a grasp of what could go wrong:
PHP Constant - self/static
But, regardless, what I could not see was the constant itself defined within the User class, which I saw belonged to your namespace.
So, in order to use it, please define it like:
class User
{
const ROLE_DEFAULT = "ROLE_DEFAULT";
// ....
// The rest of your code
}
And then use it like:
if ($role === self::ROLE_DEFAULT) {
return $this;
}
Though, I am not sure why would you attempt to add that role in your controller at the first place, however, I leave to you to you to decide.
Hope this helps a bit...
I'm new here and I'm from abroad, so sorry for my mistakes in English. Okey, so I have problem with login system in my first application in Symfony. Before I was writing some simple apps in Laravel. But here, I don't know what's wrong. I made new bundle and my login system stop working. I can put data in form, but when I pass it to authorization, website redirects me to login page and I'm not logged in. This is my code:
Controller:
<?php
// src/AppBundle/Controller/RegistrationController.php
namespace AppBundle\Controller;
use AppBundle\Form\UserType;
use AppBundle\Entity\User;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class RegistrationController extends Controller
{
/**
* #Route("/register", name="user_registration")
*/
public function registerAction(Request $request)
{
// 1) build the form
$user = new User();
$form = $this->createForm(UserType::class, $user);
// 2) handle the submit (will only happen on POST)
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// 3) Encode the password (you could also do this via Doctrine listener)
$password = $this->get('security.password_encoder')
->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
// 4) save the User!
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
// ... do any other work - like sending them an email, etc
// maybe set a "flash" success message for the user
return $this->redirectToRoute('homepage');
}
return $this->render(
'registration/registration.html.twig',
array('form' => $form->createView())
);
}
/**
* #Route("/login", name="user_login")
*/
public function loginAction(Request $request) {
$authenticationUtils = $this->get('security.authentication_utils');
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('registration/login.html.twig', array(
'last_username' => $lastUsername,
'error' => $error,
));
}
/**
* #Route("/login_check", name="user_login_check")
*/
public function loginCheckAction() {
throw new \Exception('This should never be reached!');
}
/**
* #Route("/logout", name="user_logout")
*/
public function logoutAction()
{
throw new \Exception('This should never be reached!');
}
/**
* #Route("/login_failed", name="user_login_fail")
*/
public function loginError() {
return new Response('ERROR');
}
}
security.yml
# To get started with security, check out the documentation:
# http://symfony.com/doc/current/book/security.html
security:
encoders:
# Our user class and the algorithm we'll use to encode passwords
# http://symfony.com/doc/current/book/security.html#encoding-the-user-s-password
AppBundle\Entity\User: bcrypt
# http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
providers:
our_db_provider:
entity:
class: AppBundle:User
property: username
firewalls:
default:
anonymous: ~
http_basic: ~
provider: our_db_provider
form_login:
login_path: user_login
check_path: user_login_check
failure_path: user_login_fail
logout:
path: user_logout
target: homepage
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
access_control:
- { path: ^/admin, roles: ROLE_ADMIN}
# activate different ways to authenticate
# http_basic: ~
# http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate
# form_login: ~
# http://symfony.com/doc/current/cookbook/security/form_login_setup.html
User Entity
<?php
namespace AppBundle\Entity;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
/**
* User
*/
class User implements AdvancedUserInterface, \Serializable
{
/**
* #var int
*/
private $id;
/**
* #var string
*/
private $username;
/**
* #var string
*/
private $password;
/**
* #var string
*/
private $email;
/**
* #var bool
*/
private $isActive;
/**
* #var string
*/
private $plainPassword;
public function __construct() {
$this->setIsActive(TRUE);
}
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set username
*
* #param string $username
*
* #return User
*/
public function setUsername($username)
{
$this->username = $username;
return $this;
}
/**
* Get username
*
* #return string
*/
public function getUsername()
{
return $this->username;
}
/**
* Set password
*
* #param string $password
*
* #return User
*/
public function setPassword($password)
{
$this->password = $password;
return $this;
}
/**
* Get password
*
* #return string
*/
public function getPassword()
{
return $this->password;
}
/**
* Set email
*
* #param string $email
*
* #return User
*/
public function setEmail($email)
{
$this->email = $email;
return $this;
}
/**
* Get email
*
* #return string
*/
public function getEmail()
{
return $this->email;
}
/**
* Set isActive
*
* #param boolean $isActive
*
* #return User
*/
public function setIsActive($isActive)
{
$this->isActive = $isActive;
return $this;
}
/**
* Get isActive
*
* #return bool
*/
public function getIsActive()
{
return $this->isActive;
}
public function getRoles()
{
return array('ROLE_USER');
}
public function eraseCredentials()
{
}
public function serialize()
{
return serialize(array(
$this->id,
$this->username,
$this->password,
));
}
public function unserialize($serialized)
{
list (
$this->id,
$this->username,
$this->password,
) = unserialize($serialized);
}
public function getSalt()
{
// The bcrypt algorithm doesn't require a separate salt.
// You *may* need a real salt if you choose a different encoder.
return null;
}
public function getPlainPassword()
{
return $this->plainPassword;
}
public function setPlainPassword($password)
{
$this->plainPassword = $password;
}
public function isAccountNonExpired()
{
return true;
}
public function isAccountNonLocked()
{
return true;
}
public function isCredentialsNonExpired()
{
return true;
}
public function isEnabled()
{
return $this->isActive;
}
}
Thanks for your help.