So my code is pretty straightforward, I have a login form, an authenticator, and a login method in the controller.
In my authenticator class, when user credentials are wrong, I store the email used in the session. See below ( specifically the getCredentials method ) :
<?php
namespace AppBundle\Security;
use Psr\Log\LoggerInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use AppBundle\Form\LoginFormType;
class LoginFormAuthenticator extends AbstractGuardAuthenticator
{
private $passwordEncoder;
private $logger;
private $router;
private $formFactory;
public function __construct(FormFactoryInterface $formFactory, LoggerInterface $logger,UserPasswordEncoderInterface $passwordEncoder, RouterInterface $router)
{
$this->formFactory = $formFactory;
$this->router = $router;
$this->logger = $logger;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request)
{
return ($request->getPathInfo() == '/login' && $request->isMethod('POST'));
}
public function getCredentials(Request $request)
{
$form = $this->formFactory->create(LoginFormType::class);
$form->handleRequest($request);
$data = $form->getData();
**$request->getSession()->set(
Security::LAST_USERNAME,
$data['email']
);**
$test = $request->getSession()->get(Security::LAST_USERNAME);
$this->logger->info('email : '.$test);
return $data;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$email = $credentials['email'];
$this->logger->info('email getUser : '.$email);
if (null === $email) {
return;
}
return $userProvider->loadUserByUsername($email);
}
public function checkCredentials($credentials, UserInterface $user)
{
$plainPassword = $credentials['password'];
$encoder = $this->passwordEncoder;
if(!$encoder->isPasswordValid($user, $plainPassword)) {
return new Response('Password Invalid');
}
// return true to cause authentication success
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// on success, let the request continue
return new Response("<html><head></head><body>SUCCESS</body></html>");
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
$request->getSession()->set(Security::LAST_USERNAME, $request->get('email'));
$response = new RedirectResponse($this->router->generate('security_login'));
}
/**
* Called when authentication is needed, but it's not sent
*/
public function start(Request $request, AuthenticationException $authException = null)
{
return new Response('Authentication Required');
}
public function supportsRememberMe()
{
return false;
}
}
So far so good, the email is stored in the session. ( See the getCredentials method ).
However, the same value stored in the session is empty when accessed from the login controller below :
/**
* #Route("/login", name="security_login")
*/
public function loginAction(Request $request, AuthenticationUtils $authUtils)
{
$error = $authUtils->getLastAuthenticationError();
$last_username = $authUtils->getLastUsername();
$logger->info('email in SESSION : '.$last_username);
$form = $this->createForm(LoginFormType::class,[
'email' => $last_username
]);
return $this->render('security/login.html.twig', [
'loginForm' => $form->createView(),
'error'=> $error
]);
}
It's just empty, so I'm not sure what's happening here or what I'm doing wrong.
Please advise.
Thanks!
Related
I have a problem with Symfony (4.4) authentication. I want to make an authentication based on the email and the company of the user. This means that you can have several times the same email but with a different company.
I use the following guardAuthenticator for the connection:
<?php
namespace App\Security;
use App\Entity\Company;
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 AppLoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
private $passwordEncoder;
private $request;
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)
{
$this->request = $request;
return self::LOGIN_ROUTE === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'email' => $request->request->get('_username'),
'slug' => $request->get('slug'),
'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();
}
$company = $this->entityManager->getRepository(Company::class)->findOneBy(['slug' => $credentials['slug']]);
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email'], 'company' => $company]);
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)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return;
// 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, ['slug' => $this->request->get('slug')]);
}
}
If I do a dump($user); in getUser or a dump($token, $providerKey); in onAuthenticationSuccess I find the right user. But once the connection is over, when I arrive on another page I am connected with another user with the same email but in a different company.
I think the problem comes from the refresh user that has to be done only on the email, do you know how I can change that?
Thanks to you
You will add company on getcredentials() and use it on the get user function
Try this :
public function getCredentials(Request $request)
{
$company = $this->entityManager->getRepository(Company::class)->findOneBy(['slug' => $request->get('slug')]);
$credentials = [
'email' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
'csrf_token' => $request->request->get('_csrf_token'),
'company' => $company
];
$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'], 'company' => $credentials['company']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Email could not be found.');
}
return $user;
}
Symfony 5 login not showing any errors on login page with invalid credential and redirected to login page if it's correct credential.
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']);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('reports'));
}
Here is an example of Security Controller
<?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")
* #param AuthenticationUtils $authenticationUtils
* #return Response
*/
public function login(AuthenticationUtils $authenticationUtils): Response
{
if ($this->container->get('security.authorization_checker')->isGranted('ROLE_USER')) {
return $this->redirectToRoute('car.index');
}
if ($this->getUser()) {
return $this->redirectToRoute('car.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.');
}
}
I have an issue with the Authentication Errors when login in my app in Symfony 4.
I can perfectly login with all users, but when I try to fail the login on purpose I don't get any error message neither the $error fills. Here I post the code of my LoginFormAuthenticator and my securityController.
LoginFormAuthenticator
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
/**
* #var UserRepository
*/
private $userRepository;
/**
* #var RouterInterface
*/
private $router;
/**
* #var CsrfTokenManagerInterface
*/
private $csrfTokenManager;
/**
* #var UserPasswordEncoderInterface
*/
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)
{
$credentials = [
'cargo' => $request->request->get('cargo'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['cargo']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
return $this->userRepository->findOneBy(['cargo' => $credentials['cargo']]);
}
public function checkCredentials($credentials, UserInterface $user)
{
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
// todo
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return new RedirectResponse($this->router->generate('app_homepage'));
}
public function start(Request $request, AuthenticationException $authException = null)
{
// todo
}
public function supportsRememberMe()
{
// todo
}
protected function getLoginUrl()
{
return $this->router->generate('app_login');
}
}
login function:
/**
* #Route("/login", name="app_login")
*/
public function login(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', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
Login HTML twig
<form class="form-signin" method="post">
{% if error %}
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
<h3>{{ error.messageKey }}</h3>
{% endif %}
Thanks!
The onAuthenticationFailure method is empty. See parent::onAuthenticationFailure.
Method should returns Response object.
Here is my subscriber class. I want to get the user email to him an email.
I use here the EntityManagerInterface
use Doctrine\ORM\EntityManagerInterface;
final class RegisterMailSubscriber implements EventSubscriberInterface
{
private $mailer;
public function __construct(\Swift_Mailer $mailer, EntityManagerInterface $entityManager)
{
$this->mailer = $mailer;
$this->repository= $entityManager->getRepository('AppEntity:User');
}
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => ['sendMail', EventPriorities::POST_WRITE],
];
}
public function sendMail(ViewEvent $event): void
{
$user = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
if (!$user instanceof User || Request::METHOD_POST !== $method) {
return;
}
$userInfo = $this->repository->find($user->getId());
}
}
You need to import all the dependencies used as User, Request, ViewEvent, KernelEvent, etc.
By the way, is a good practice to import the repository (UserRepository) and not the entityManager, but you dont need it because you have the $user already. You dont need to find it again.
I think this should be enough if you have the user classes in those namespaces (locations):
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use ApiPlatform\Core\EventListener\EventPriorities;
use App\Entity\User;
final class RegisterMailSubscriber implements EventSubscriberInterface
{
private $mailer;
public function __construct(\Swift_Mailer $mailer)
{
$this->mailer = $mailer;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => ['sendMail', EventPriorities::POST_WRITE],
];
}
public function sendMail(ViewEvent $event): void
{
$user = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
if (!$user instanceof User || Request::METHOD_POST !== $method) {
return;
}
$userEmail = $user->getEmail(); //for example. You got the user 5 lines before.
}
}
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);
}
}