Is it possible to have 2 entities that implement UserInterface?
How to use it on my guard to make it check in both classes and with the same firewall?
The idea is that companies can have their own CVTheque or shared it (CVTheque has a OneToMany relationship with Company).
I would like to have Candidate and User entities.
CVTheque -> OneToMany -> Candidate
User -> ManyToOne -> Company.
Candidate and User will used the same login form to be authenticated on the app. So I don't know if it's possible and how to implement this on my guard authenticator.
They will be redirected to their own dashboard depending to instance of connected user (Candidate or User).
Recently handled a similar scenario.
In my case, just created a chain_provider to encapsulate all entities needed:
providers:
chain_provider:
chain:
providers: [provider_one, provider_two]
provider_one:
entity:
class: App\Entity\ProviderOne
property: username
provider_two:
entity:
class: App\Entity\ProviderTwo
property: email
security:
firewalls:
secured_area:
# ...
pattern: ^/login
provider: chain_provider
I believe it is possible, let's take basic user authentication and try to tweak it
<?php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
class LoginAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
private $encoder;
public function __construct(
EntityManagerInterface $entityManager,
UrlGeneratorInterface $urlGenerator,
CsrfTokenManagerInterface $csrfTokenManager,
UserPasswordEncoderInterface $encoder)
{
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->encoder = $encoder;
$this->eventDispatcher = $eventDispatcher;
}
public function supports(Request $request)
{
return (
'login' === $request->attributes->get('_route')&& $request->isMethod('POST') // here you need to specify the other login route if you want to have 2 seperate ones
);
}
public function getCredentials(Request $request)
{
$credentials = [
'username' => $request->request->get('username'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['username']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $this->entityManager->getRepository(User::class)->findOneBy(['username' => $credentials['username']]);
if (!$user) { // here look for user in the 2nd entity, if it will still be null throw the exception
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Username could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
return $this->encoder->isPasswordValid($user, $credentials['password'], $user->getSalt());
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('account_index'));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate('login');
}
}
And that's probably it, we change the authenticator to support 2 routes (login one, login two), and if the first entity does not match username and password try to find user in the other one, you might as well add a hidden input or add attribute in request listener based on the route retrieve it using RequestStack or however you want to indicate each entity.
Related
I'm working on a mini web project and i'm building it in symfony, so after i created registeration and login form, i realised that after registration user won't be directly authenticated, so i made some research to see how it works but i'm facing some problems and i'm new to this framework.
Here's the controller code (the users class is called Players in my app because i'm developping an escape game)
<?php
namespace App\Controller;
use App\Entity\Items;
use App\Entity\Players;
use App\Entity\Inventaire;
use App\Security\TokenAuthenticator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class PlayerController extends AbstractController
{
private $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
}
/**
* #Route("/player", name="player")
*/
public function index(): Response
{
return $this->render('player/index.html.twig', [
'controller_name' => 'PlayerController',
]);
}
/**
* #Route("/login_Player", name="login_Player")
*/
public function loginPlayer(){
return $this->render("app/player_interface.html.twig");
}
/**
* #Route("/redirect_player", name="redirect_player")
*/
public function redirectPlayer(){
$player = $this->getUser();
if($player){
return $this->render("app/player_interface.html.twig",['player'=>$player]);
}
else{
return $this-render("app/home.html.twig");
}
}
/**
* #Route("/logout_Player", name="logout_Player")
*/
public function logoutPlayer(){
return $this->render("app/home.html.twig");
}
/**
* #Route("/create_interface", name="create_Player")
*/
public function createPlayer(Request $request,EntityManagerInterface $em,UserPasswordEncoderInterface $encoder,GuardAuthenticatorHandler $guardHandler,TokenAuthenticator $authenticator): Response
{
$repository = $this->getDoctrine()->getRepository(Players::class);
if($request->request->count()>0){
if(($repository->findBy(["username"=>$request->request->get("pseudo")])) || ($repository->findBy(["email"=>$request->request->get("mail")]))){
return $this->render("app/home.html.twig",["msg"=>"Id ou mail déja utilisé!"]);
}
else{
$coord=[48.8413672,2.4223428];
$player = new Players();
$player->setUsername($request->request->get("pseudo"))
->setPassword($request->request->get("pass"))
->setEmail($request->request->get("mail"));
$player->setLongitude($coord[1]);
$player->setLatitude($coord[0]);
$inventaire=new Inventaire();
$inventaire->setPlayerId($player);
$player->setInventaire($inventaire);
$this->em=$em;
$hash=$encoder->encodePassword($player, $player->getPassword());
$player->setPassword($hash);
$em->persist($player);
$em->persist($inventaire);
$em->flush();
return $guardHandler->authenticateUserAndHandleSuccess($player,$request,$authenticator,'main');
//return $this->render("app/player_interface.html.twig",['player'=>$player]);//'inventaire'=>$inventaire]);
}
}
}
/**
* #Route("/add_item",name="add_item")
*/
public function addItem(Request $request,EntityManagerInterface $em){
if ($request->isXmlHttpRequest()){
$this->em=$em;
$player = $this->getUser();
$inventaire=$player->getInventaire();
$item=new Items();
$item->setDescription($request->request->get("description"))
->setVisibility($request->request->get("visibility"))
->setNiveau($request->request->get("icon_id"));
$inventaire->addItemId($item);
$em->flush();
return new JsonResponse(array("player"=>$player,"inventaire"=>$inventaire->getItem_id()));
}
else{
return $this->render("app/player_interface.html.twig");
}
}
/**
* #Route("/move_player",name="move_player")
*/
public function movePlayer(Request $request,EntityManagerInterface $em){
if ($request->isXmlHttpRequest()){
$this->em=$em;
$player = $this->getUser();
$player->setLongitude($request->request->get("longitude"))
->setLatitude($request->request->get("latitude"));
$em->flush();
return new JsonResponse(array("player"=>$player));
}
else{
return $this->render("app/player_interface.html.twig");
}
}
/**
* #Route("affiche_scenario", name="affiche_scenario")
*/
public function afficheScenario(Request $request){
if ($request->isXmlHttpRequest()){
$repository = $this->getDoctrine()->getRepository(Scenarios::class);
return new JsonResponse($repository->findBy($request->request->get("num_scenario")));
}
else{
return $this->render("app/player_interface.html.twig");
}
}
}
?>
and this is the tokenauthenticator class:
<?php
// src/Security/TokenAuthenticator.php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class TokenAuthenticator extends AbstractGuardAuthenticator
{
private $em;
protected $router;
public function __construct(EntityManagerInterface $em,HttpUtils $httpUtils, $router)
{
$this->em = $em;
$this->router = $router;
}
/**
* Called on every request to decide if this authenticator should be
* used for the request. Returning `false` will cause this authenticator
* to be skipped.
*/
public function supports(Request $request)
{
return $request->headers->has('X-AUTH-TOKEN');
}
/**
* Called on every request. Return whatever credentials you want to
* be passed to getUser() as $credentials.
*/
public function getCredentials(Request $request)
{
return $request->headers->get('X-AUTH-TOKEN');
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
if (null === $credentials) {
// The token header was empty, authentication fails with HTTP Status
// Code 401 "Unauthorized"
return null;
}
// The "username" in this case is the apiToken, see the key `property`
// of `your_db_provider` in `security.yaml`.
// If this returns a user, checkCredentials() is called next:
return $userProvider->loadUserByUsername($credentials);
}
public function checkCredentials($credentials, UserInterface $user)
{
// Check credentials - e.g. make sure the password is valid.
// In case of an API token, no credential check is needed.
// Return `true` to cause authentication success
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
/*if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}*/
return new RedirectResponse($this->router->generate('redirect_player'));
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$data = [
// you may want to customize or obfuscate the message first
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
// or to translate this message
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
/**
* Called when authentication is needed, but it's not sent
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$data = [
// you might translate this message
'message' => 'Authentication Required'
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
public function supportsRememberMe()
{
return false;
}
}
?>
And finaly the security.yaml file
security:
encoders:
App\Entity\Players:
algorithm: bcrypt
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
users_in_memory: { memory: null }
in_database:
entity:
class: App\Entity\Players
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
#provider: users_in_memory
provider: in_database
form_login:
login_path: login_Player
check_path: login_Player
#failure_handler: App\Security\authentication_failure_handler
csrf_token_generator: security.csrf.token_manager
failure_path: home
guard:
authenticators:
- App\Security\TokenAuthenticator
logout:
path: logout_Player
target: home
# 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 }
The TokenAuthenticator class i found it on symfony and another tutorial so i don't really understand the mechanism and when i try to register a user now, the app shows me an error:
Cannot autowire service "App\Security\TokenAuthenticator": argument "$router" of method "__construct()" has no type-hint, you should configure its value explicitly.
Thanks in advance everyone and have a good day.
https://symfony.com/doc/current/service_container/autowiring.html
Autowiring allows you to manage services in the container with minimal configuration. It reads the type-hints on your constructor (or other methods) and automatically passes the correct services to each method. Symfony’s autowiring is designed to be predictable: if it is not absolutely clear which dependency should be passed, you’ll see an actionable exception.
You must type hint your arguments passed to the constructor so that symfony can pass them to you automatically thanks to autowiring:
// others uses
use Symfony\Component\Routing\RouterInterface;
class TokenAuthenticator extends AbstractGuardAuthenticator
{
private $em;
protected $router;
public function __construct(EntityManagerInterface $em, HttpUtils $httpUtils, RouterInterface $router)
{
$this->em = $em;
$this->router = $router;
}
Or if you want juste to generate urls, you cant use the UrlGeneratorInterface instead of the router:
// ...
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class TokenAuthenticator extends AbstractGuardAuthenticator
{
private $em;
protected $urlGenerator;
public function __construct(EntityManagerInterface $em, HttpUtils $httpUtils, UrlGeneratorInterface $urlGenerator)
{
$this->em = $em;
$this->urlGenerator = $urlGenerator;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return new RedirectResponse($this->urlGenerator->generate('redirect_player'));
}
I am trying to integrate my legacy database password validator, for that I have configured a custom encoding password: https://symfony.com/doc/current/security/named_encoders.html
I am using symfony 5.1 and php 7.4.
It is my security.yaml
security:
encoders:
App\Entity\User:
algorithm: auto
#para oracle puse auto
app_encoder:
id: 'App\Security\Encoder\MyCustomPasswordEncoder'
#para oracle
# 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: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
provider: app_user_provider
guard:
authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: app_logout
#target: app_logout
remember_me:
secret: '%kernel.secret%'
lifetime: 2592000 # 30 days in seconds
# 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 }
This is my password custom encoder src/Security/Encoder/MyCustomPasswordEncoder.php
<?php
namespace App\Security\Encoder;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class MyCustomPasswordEncoder implements UserPasswordEncoderInterface
{
/**
* {#inheritdoc}
*/
public function encodePassword(UserInterface $user, string $plainPassword)
{
$encoder = $this->encoderFactory->getEncoder($user);
return $encoder->encodePassword($plainPassword, $user->getSalt());
}
/**
* {#inheritdoc}
*/
public function isPasswordValid(UserInterface $user, string $raw)
{
if (null === $user->getPassword()) {
return false;
}
die('Esta usando la mia');
$encoder = $this->encoderFactory->getEncoder($user);
return $encoder->isPasswordValid($user->getPassword(), $raw, $user->getSalt());
}
/**
* {#inheritdoc}
*/
public function needsRehash(UserInterface $user): bool
{
if (null === $user->getPassword()) {
return false;
}
$encoder = $this->encoderFactory->getEncoder($user);
return $encoder->needsRehash($user->getPassword());
}
}
this is my src/Security/LoginFormAuthenticator.php
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use App\Repository\UserRepository;
use Symfony\Component\Routing\RouterInterface; //segudo parametro constructor
use Symfony\Component\Security\Core\Security; //Security::
use Symfony\Component\HttpFoundation\RedirectResponse; //redirect response
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; //CSR Token
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; //password
use App\Security\Encoder\MyCustomPasswordEncoder;
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
private $userRepository;
private $router;
private $csrfTokenManager;
private $passwordEncoder;
public function __construct(UserRepository $userRepository, RouterInterface $router, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
{
$this->userRepository = $userRepository;
$this->router = $router;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request)
{
return $request->attributes->get('_route') === 'app_login'
&& $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
// todo
//dd($request->request->all()); //esto es lo mismo que die(dump())
/*return [
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
];*/
$credentials = [
'email' => $request->request->get('email'),
'csrf_token' => $request->request->get('_csrf_token'),
'password' => $request->request->get('password'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['email']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
// todo
//dd($credentials);
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
return $this->userRepository->findOneBy(['email' => $credentials['email']]);
}
public function checkCredentials($credentials, UserInterface $user)
{
// todo
//dd($user);
//return true;
//dd($this);
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
// todo
//dd('Success');
return new RedirectResponse($this->router->generate('app_homepage'));
}
protected function getLoginUrl()
{
// TODO: Implement getLoginUrl() method.
return $this->router->generate('app_login');
}
}
My problem is that it does not run my custom password validator it is taking the default password validator.
Thank you.
Things can get confusing because there are two interfaces involved: PasswordEncoderInterface and UserPasswordEncoderInterface. There is a tendency to want to create a custom UserPasswordEncoderInterface because, well, you are encoding a user password. But in fact the UserPasswordEncoder object is basically just a wrapper for the underlying PasswordEncoders.
So you need to implement your legacy database password validator in a PasswordEncoder object:
namespace App\Security;
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
class MyPasswordEncoder implements PasswordEncoderInterface
{
public function encodePassword(string $raw, ?string $salt)
{
return 'ENCODED' . $raw;
}
public function isPasswordValid(string $encoded, string $raw, ?string $salt)
{
return true;
}
public function needsRehash(string $encoded): bool
{
return false;
}
}
Next you need to tell Symfony to use your custom encoder for a given type of user:
# config/packages/security.yaml
security:
encoders:
App\Entity\User:
id: App\Security\MyPasswordEncoder
At this point you can confirm that your encoder is being used with:
$ bin/console security:encode-password xxx
Encoder used App\Security\MyPasswordEncoder
Encoded password ENCODEDxxx
That should be enough to get your going. But at the risk of adding even more confusion, here is a little test command which attempts to show the relationship UserPasswordEncoderInterface, PasswordEncoderInterface and the EncoderFactoryInterface which essentially picks the correct encoder for a given user based on the security.yaml mappings:
class UserCommand extends Command
{
protected static $defaultName = 'app:user';
private $encoderFactory;
private $userPasswordEncoder;
public function __construct(EncoderFactoryInterface $encoderFactory, UserPasswordEncoderInterface $userPasswordEncoder)
{
parent::__construct();
$this->encoderFactory = $encoderFactory;
$this->userPasswordEncoder = $userPasswordEncoder;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$encoder = $this->encoderFactory->getEncoder(User::class);
echo get_class($encoder) . "\n";
$user = new User();
$encoded = $this->userPasswordEncoder->encodePassword($user,'zzz');
echo $encoded . "\n";
return Command::SUCCESS;
}
}
Also wanted to point out that the link in the question points to using named encoders. Named encoders allow mapping multiple encoders to a single entity class and then allowing the entity to pick the encoder based on some property. For example, an admin user might use a different encoder than a regular user. Named encoders are not applicable to this use case.
You might however want to take a look at how to automatically upgrade passwords. Once configured users can login with the legacy encoder and then be automatically updated to a new encoder.
I am trying to build a login page register user its working after I wanted to login them in so I used the make: Auth seemed but now Symfony 5 keeps redirecting from the register route to login without saving the users in the DB first when the form is submitted it was working before i ran the make auth cmd
most of the code regaring the user auth
this is the guide i used https://symfony.com/doc/current/security/form_login_setup.html
on the Symfony docs
i guess that's why it messing up
When you submit the form, the LoginFormAuthenticator will intercept
the request, read the email (or whatever field you’re using) &
password from the form, find the User object, validate the CSRF token
and check the password
security:
encoders:
App\Entity\User:
algorithm: auto
providers:
users:
entity:
class: App\Entity\User
property: Username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
access_denied_handler: App\Security\AccessDeniedHandler
anonymous: false
lazy: true
provider: users
form_login:
login_path: app_login
check_path: app_login
guard:
authenticators:
- App\Security\AuthAuthenticator
logout:
path: app_logout
# 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: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
`
<?php
namespace App\Controller;
use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Security\EmailVerifier;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mime\Address;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
class RegistrationController extends AbstractController
{
private $emailVerifier;
public function __construct(EmailVerifier $emailVerifier)
{
$this->emailVerifier = $emailVerifier;
}
/**
* #Route("/register", name="app_register")
*/
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder): Response
{
$user = new User();
$form = $this->createForm(RegistrationFormType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// encode the plain password
$user->setPassowrd(
$passwordEncoder->encodePassword(
$user,
$form->get('plainPassword')->getData()
)
);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
// generate a signed url and email it to the user
$this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
(new TemplatedEmail())
->from(new Address('root#root.com', 'root'))
->to($user->getEmail())
->subject('Please Confirm your Email')
->htmlTemplate('registration/confirmation_email.html.twig')
);
// do anything else you need here, like send an email
return $this->redirectToRoute('index');
}
return $this->render('registration/register.html.twig', [
'registrationForm' => $form->createView(),
]);
}
/**
* #Route("/verify/email", name="app_verify_email")
*/
public function verifyUserEmail(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
// validate email confirmation link, sets User::isVerified=true and persists
try {
$this->emailVerifier->handleEmailConfirmation($request, $this->getUser());
} catch (VerifyEmailExceptionInterface $exception) {
$this->addFlash('verify_email_error', $exception->getReason());
return $this->redirectToRoute('app_register');
}
// #TODO Change the redirect on success and handle or remove the flash message in your templates
$this->addFlash('success', 'Your email address has been verified.');
return $this->redirectToRoute('app_register');
}
}
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends AbstractController
{
/**
* #Route("/login", name="app_login")
*/
public function login(AuthenticationUtils $authenticationUtils): Response
{
if ($this->getUser()) {
return $this->redirectToRoute('index');
}
// 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', ['last_username' => $lastUsername, 'error' => $error]);
}
/**
* #Route("/logout", name="app_logout")
*/
public function logout()
{
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
}
{% extends 'base.html.twig' %}
{% block title %}Register{% endblock %}
{% block body %}
{% for flashError in app.flashes('verify_email_error') %}
<div class="alert alert-danger" role="alert">{{ flashError }}</div>
{% endfor %}
<h1>Register</h1>
{{ form_start(registrationForm) }}
{{ form_row(registrationForm.Username) }}
{{ form_row(registrationForm.plainPassword, {
label: 'Password'
}) }}
{{ form_row(registrationForm.agreeTerms) }}
<button type="submit" class="btn">Register</button>
{{ form_end(registrationForm) }}
{% endblock %}
<?php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
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\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 AuthAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
private $passwordEncoder;
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
{
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request)
{
return self::LOGIN_ROUTE === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'Username' => $request->request->get('Username'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['Username']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $this->entityManager->getRepository(User::class)->findOneBy(['Username' => $credentials['Username']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException(' Username could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
/**
* 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)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
// For example : return new RedirectResponse($this->urlGenerator->generate('some_route'));
throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
}
You seem to lack the csrf_token hidden field on your twig form as is explained on the Symfony doc link you provided : How to build a login form
<input type="hidden" name="_csrf_token"
value="{{ csrf_token('authenticate') }}"
>
Although i wouldn't recommend it, another solution if your application is not critical would be to disable csrf_checking - just remove all mentions of CSRF from your AuthAuthenticator.
I created a login form for my Symfony 5 project.
I want redirect anonymous user to custom page (not the login page) when the user does not have access to my admin panel.
The application controller:
// TestController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
class TestController extends AbstractController
{
/**
* #Route("/admin/panel", name="test")
* #IsGranted("ROLE_ADMIN")
*/
public function index()
{
return $this->render('test/index.html.twig', [
'controller_name' => 'TestController',
]);
}
}
And my configuration settings:
# security.yaml
security:
encoders:
App\Entity\Admin:
algorithm: auto
providers:
admin:
entity:
class: App\Entity\Admin
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
admin:
pattern: ^/admin/
guard:
authenticators:
- App\Security\AdminLoginFormAuthenticator
logout:
path: app_logout
main:
anonymous: lazy
My application authenticator:
// AdminLoginFormAuthenticator.php
namespace App\Security;
use App\Entity\Admin;
use Doctrine\ORM\EntityManagerInterface;
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\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 AdminLoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{
use TargetPathTrait;
private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
private $passwordEncoder;
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
{
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request)
{
return 'app_admin' === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'username' => $request->request->get('username'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['username']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $this->entityManager->getRepository(Admin::class)->findOneBy(['username' => $credentials['username']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('...!');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
/**
* 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)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('homepage'));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate('app_admin');
}
}
Your AdminLoginFormAuthenticator extends on AbstractFormLoginAuthenticator, which includes the following:
/**
* Override to control what happens when the user hits a secure page
* but isn't logged in yet.
*
* #return RedirectResponse
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$url = $this->getLoginUrl();
return new RedirectResponse($url);
}
Reading this, what you need to do is obvious.
Just create your own start() method to control what happens (e.g. where are they redirected) if they do not have access.
namespace App\Security;
class AdminLoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{
// the rest of your class
public function start(Request $request, AuthenticationException $authException = null)
{
$url = 'https:://example.com/whateverurlyouwanttoredirectto';
return new RedirectResponse($url);
}
}
I'd like to log the user in right after the registration process, without passing by the login form.
Is this possible ? I've found a solution with FOSUserBundle, but I'm not using it on the project I'm actually working on.
Here is my security.yml, I'm working with two firewalls.
The plain text encoder is just for testing.
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
Ray\CentralBundle\Entity\Client: md5
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
in_memory:
users:
admin: { password: admin, roles: [ 'ROLE_ADMIN' ] }
entity:
entity: { class: Ray\CentralBundle\Entity\Client, property: email }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
user_login:
pattern: ^/user/login$
anonymous: ~
admin_login:
pattern: ^/admin/login$
anonymous: ~
admin:
pattern: ^/admin
provider: in_memory
form_login:
check_path: /admin/login/process
login_path: /admin/login
default_target_path: /admin/dashboard
logout:
path: /admin/logout
target: /
site:
pattern: ^/
provider: entity
anonymous: ~
form_login:
check_path: /user/login/process
login_path: /user/login
default_target_path: /user
logout:
path: /user/logout
target: /
access_control:
- { path: ^/user/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/user, roles: ROLE_USER }
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
Yes, you can do this via something similar to the following:
use Symfony\Component\EventDispatcher\EventDispatcher,
Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken,
Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
public function registerAction()
{
// ...
if ($this->get("request")->getMethod() == "POST")
{
// ... Do any password setting here etc
$em->persist($user);
$em->flush();
// Here, "public" is the name of the firewall in your security.yml
$token = new UsernamePasswordToken($user, $user->getPassword(), "public", $user->getRoles());
// For older versions of Symfony, use security.context here
$this->get("security.token_storage")->setToken($token);
// Fire the login event
// Logging the user in above the way we do it doesn't do this automatically
$event = new InteractiveLoginEvent($request, $token);
$this->get("event_dispatcher")->dispatch("security.interactive_login", $event);
// maybe redirect out here
}
}
The event firing at the end isn't automatically done when you set a token into the context, whereas it would be normally when using eg a login form or similar. Hence the reason for including it here. You may need to adjust the type of token used, depending on your use case - the UsernamePasswordToken shown above is a core token, but you can use others if required.
Edit: Adjusted the above code to explain the 'public' parameter and also add in the roles of the user into the token creation, based on Franco's comment below.
If you are on symfony ^6.2, you can use Security::login().
For older versions (symfony ^5.4, ^6.0, ^6.1), the following works:
public function login(User $user, Request $request, UserCheckerInterface $checker, UserAuthenticatorInterface $userAuthenticator, FormLoginAuthenticator $formLoginAuthenticator): void
{
$checker->checkPreAuth($user);
$userAuthenticator->authenticateUser($user, $formLoginAuthenticator, $request);
}
You may choose to move this functionality into a service so dependency injection is easier:
# config/services.yaml
services:
App\Service\LoginService:
arguments:
$formLoginAuthenticator: '#security.authenticator.form_login.main'
# src/Service/LoginService.php
namespace App\Service;
use App\Entity\User;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator;
class LoginService
{
private UserCheckerInterface $checker;
private UserAuthenticatorInterface $userAuthenticator;
private FormLoginAuthenticator $formLoginAuthenticator;
/**
* #param UserCheckerInterface $checker
* #param UserAuthenticatorInterface $userAuthenticator
* #param FormLoginAuthenticator $formLoginAuthenticator
*/
public function __construct(UserCheckerInterface $checker, UserAuthenticatorInterface $userAuthenticator, FormLoginAuthenticator $formLoginAuthenticator)
{
$this->checker = $checker;
$this->userAuthenticator = $userAuthenticator;
$this->formLoginAuthenticator = $formLoginAuthenticator;
}
public function login(User $user, Request $request): void
{
$this->checker->checkPreAuth($user);
$this->userAuthenticator->authenticateUser($user, $this->formLoginAuthenticator, $request);
}
}
Source is an RFC requesting an easier way for programmatic login. This has been implemented, was released with symfony 6.2.
The accepted version will not work with symfony 3.3. User will be authenticated in the next request instead of the current one. The reason is that ContextListener checks for previous session existence and if not exists it will clear the security TokenStorage. The only way around this (hackish as hell) is to fake the existence of previous session by manually initialising the session (and cookie) on the current request.
Let me know if you find a better solution.
BTW I am not sure if this should be merged with the accepted solution.
private function logUserIn(User $user)
{
$token = new UsernamePasswordToken($user, null, "common", $user->getRoles());
$request = $this->requestStack->getMasterRequest();
if (!$request->hasPreviousSession()) {
$request->setSession($this->session);
$request->getSession()->start();
$request->cookies->set($request->getSession()->getName(), $request->getSession()->getId());
}
$this->tokenStorage->setToken($token);
$this->session->set('_security_common', serialize($token));
$event = new InteractiveLoginEvent($this->requestStack->getMasterRequest(), $token);
$this->eventDispatcher->dispatch("security.interactive_login", $event);
}
The above code assumes that your firewall name (or shared context name) is common.
Try this : For Symfony 3 users, do not forget to make this correction to test the equality of the passwords (as the method shown to test the password on this link is not working) :
$current_password = $user->getPassword();
$user_entry_password = '_got_from_a_form';
$factory = $this->get('security.encoder_factory');
$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword($user_entry_password, $user->getSalt());
if(hash_equals($current_password, $password)){
//Continue there
}
// I hash the equality process for more security
+ info : hash_equals_function
For Symfony 5, you can use out of the box functionalities to create login and registration forms.
How to create login form: https://symfony.com/doc/current/security/form_login_setup.html
How to create registration form: https://symfony.com/doc/current/doctrine/registration_form.html
Using Symfony\Component\Security\Guard\GuardAuthenticatorHandler is key point.
You can use GuardAuthenticatorHandler in registration controller after successful registration. It logs in user and redirects to page defined in onAuthenticationSuccess from LoginFormAuthenticator.
Below, I added some code snippets.
<?php
namespace App\Controller\Login;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class LoginController extends AbstractController
{
/**
* #Route("/login", name="app_login")
*/
public function login(AuthenticationUtils $authenticationUtils): Response
{
// 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', ['last_username' => $lastUsername, 'error' => $error]);
}
/**
* #Route("/logout", name="app_logout")
*/
public function logout()
{
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
}
<?php
namespace App\Security;
use App\Entity\User\User;
use Doctrine\ORM\EntityManagerInterface;
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\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 LoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{
use TargetPathTrait;
private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
private $passwordEncoder;
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
{
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request)
{
return 'app_login' === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['email']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Email could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
/**
* 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('app_homepage'));
// if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
// return new RedirectResponse($this->urlGenerator->generate('app_homepage'));
// }
//
// // For example : return new RedirectResponse($this->urlGenerator->generate('some_route'));
// throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate('app_login');
}
}
<?php
namespace App\Controller;
use App\Entity\User\User;
use App\Security\LoginFormAuthenticator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
class RegistrationController extends AbstractController
{
private EntityManagerInterface $objectManager;
private UserPasswordEncoderInterface $passwordEncoder;
private GuardAuthenticatorHandler $guardHandler;
private LoginFormAuthenticator $authenticator;
/**
* RegistrationController constructor.
* #param EntityManagerInterface $objectManager
* #param UserPasswordEncoderInterface $passwordEncoder
* #param GuardAuthenticatorHandler $guardHandler
* #param LoginFormAuthenticator $authenticator
*/
public function __construct(
EntityManagerInterface $objectManager,
UserPasswordEncoderInterface $passwordEncoder,
GuardAuthenticatorHandler $guardHandler,
LoginFormAuthenticator $authenticator
) {
$this->objectManager = $objectManager;
$this->passwordEncoder = $passwordEncoder;
$this->guardHandler = $guardHandler;
$this->authenticator = $authenticator;
}
/**
* #Route("/registration")
*/
public function displayRegistrationPage()
{
return $this->render(
'registration/registration.html.twig',
);
}
/**
* #Route("/register", name="app_register")
*
* #param Request $request
* #return Response
*/
public function register(Request $request)
{
// if (!$this->isCsrfTokenValid('sth-special', $request->request->get('token'))) {
// return $this->render(
// 'registration/registration.html.twig',
// ['errorMessage' => 'Token is invalid']
// );
// }
$user = new User();
$user->setEmail($request->request->get('email'));
$user->setPassword(
$this->passwordEncoder->encodePassword(
$user,
$request->request->get('password')
)
);
$user->setRoles(['ROLE_USER']);
$this->objectManager->persist($user);
$this->objectManager->flush();
return $this->guardHandler->authenticateUserAndHandleSuccess(
$user,
$request,
$this->authenticator,
'main' // firewall name in security.yaml
);
return $this->render('base.html.twig');
}
}
After several days of debugging and investigating I finally authenticate a user programmatically on Symfony 4.4. I guess this approach should also work on the newer versions too.
Important to get the correct name of the firewall, main in my case, in your security.yml
security:
firewalls:
main:
pattern: ^/
#...
and then pass it into the session:
$session->set('_security_main', serialize($token));
The full code of login action:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
//...
public function loginAction(
Request $request,
TokenStorageInterface $tokenStorage,
SessionAuthenticationStrategyInterface $sessionStrategy,
AuthenticationProviderManager $authManager
) {
// ...
if ($request->getMethod() == "POST") {
// Fetching user and checking password logic...
$em->flush();
// Create an authenticated token for the User.
// Here, "main" is the name of the firewall in your security.yml
$token = new UsernamePasswordToken(
$email,
$password,
'main', // firewall name in security.yaml
$user->getRoles()
);
$session = $request->getSession();
if (!$request->hasPreviousSession()) {
$request->setSession($session);
$request->getSession()->start();
$request->cookies->set($request->getSession()->getName(), $request->getSession()->getId());
}
$session->set(Security::LAST_USERNAME, $email);
// Authenticate user
$authManager->authenticate($token);
$sessionStrategy->onAuthentication($request, $token);
// For older versions of Symfony, use "security.context" here
$tokenStorage->setToken($token);
$session->set('_security_main', serialize($token));
$session->remove(Security::AUTHENTICATION_ERROR);
$session->remove(Security::LAST_USERNAME);
// Fire the login event
$event = new InteractiveLoginEvent($request, $token);
$this->get('event_dispatcher')->dispatch($event, SecurityEvents::INTERACTIVE_LOGIN);
// return success response here
}
}
$this->get('fos_user.security.login_manager')->logInUser('main', $user);
Where 'main' is the name of your firewall in security.yml, and $user is the object representing the user you want to log in.
This works in my Symfony 2.8 project, you can check for the login_manager service in your version by running php app/console debug:container.