on my new symfony 5.3 project i just implemented the new authentication system, and it works fine, but my problem is that i can't customize the authentication errors:
in the method: onAuthentificationFailure in the AbstractLoginFormAuthenticator but in my view it only displays the session error which is normal since my controller calls the getLastAuthenticationError() method.
but how could I display my custom error from my CustomUserMessageAuthenticationException in my view ?
My AbstractLoginFormAuthenticator
namespace Symfony\Component\Security\Http\Authenticator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
/**
* A base class to make form login authentication easier!
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, InteractiveAuthenticatorInterface
{
/**
* Return the URL to the login page.
*/
abstract protected function getLoginUrl(Request $request): string;
/**
* {@inheritdoc}
*
* Override to change the request conditions that have to be
* matched in order to handle the login form submit.
*
* This default implementation handles all POST requests to the
* login path (@see getLoginUrl()).
*/
public function supports(Request $request): bool
{
return $request->isMethod('POST') && $this->getLoginUrl($request) === $request->getPathInfo();
}
/**
* Override to change what happens after a bad username/password is submitted.
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
if ($request->hasSession()) {
//$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
throw new CustomUserMessageAuthenticationException('error custom ');
}
$url = $this->getLoginUrl($request);
return new RedirectResponse($url);
}
/**
* Override to control what happens when the user hits a secure page
* but isn't logged in yet.
*/
public function start(Request $request, AuthenticationException $authException = null): Response
{
$url = $this->getLoginUrl($request);
return new RedirectResponse($url);
}
public function isInteractive(): bool
{
return true;
}
}
My SécurityController:
namespace App\Controller;
use App\Entity\User;
use DateTimeImmutable;
use App\Form\RegistrationType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class SecurityController extends AbstractController {
/**
* @Route("/test", name="test")
*/
public function test(Request $request, Response $response){
return $this->render('test.html.twig');
dump($response);
}
/**
* @Route("/login", name="app_login")
*/
public function login(AuthenticationUtils $authenticationUtils): Response
{
// if ($this->getUser()) {
// return $this->redirectToRoute('target_path');
// }
// 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(): void
{
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
/**
* @Route("/Inscription", name="registration")
*/
public function registration(EntityManagerInterface $manager, Request $request, UserPasswordHasherInterface $passwordEncoder): Response
{
$user = new User;
$registrationForm = $this->createForm(RegistrationType::class, $user);
$registrationForm->handleRequest($request);
if($registrationForm->isSubmitted() && $registrationForm->isValid()){
$plainPassword = $registrationForm->getData()->getPassword();
$user->setPassword($passwordEncoder->hashPassword($user, $plainPassword));
$user->setCreatedAt( new \DateTimeImmutable('NOW'));
$user->setRoles($user->getRoles());
$manager->persist($user);
$manager->flush();
}
return $this->render('security/registration.html.twig',
['registrationForm' => $registrationForm->createView()]);
}
}
my view twig(loginform):
{% block title %}Log in!{% endblock %}
{% block body %}
<form method="post">
{% if error %}
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
{% if app.user %}
<div class="mb-3">
You are logged in as {{ app.user.username }}, <a href="{{ path('app_logout') }}">Logout</a>
</div>
{% endif %}
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
<label for="inputEmail">Email</label>
<input type="email" value="{{ last_username }}" name="email" id="inputEmail" hljs-string">" autocomplete="email" required autofocus>
<label for="inputPassword">Password</label>
<input type="password" name="password" id="inputPassword" hljs-string">" autocomplete="current-password" required>
<input type="hidden" name="_csrf_token"
value="{{ csrf_token('authenticate') }}"
>
{#
Uncomment this section and add a remember_me option below your firewall to activate remember me functionality.
See https://symfony.com/doc/current/security/remember_me.html
<div hljs-number">3">
<label>
<input type="checkbox" name="_remember_me"> Remember me
</label>
</div>
#}
<button hljs-string">" type="submit">
Sign in
</button>
</form>
{% endblock %}
CodePudding user response:
You should extend and override the AbstractLoginFormAuthenticator
as opposed to modifying it directly.
Why your approach is not working
In short, you need to throw the Exception before reaching onAuthenticationFailure()
, as the AuthenticationException
is why onAuthenticationFailure()
is called by Symfony.
The onAuthenticationFailure()
method is what handles the thrown AuthenticationException
from the AuthenticatorManager::executeAuthenticator()
process.
try {
// get the passport from the Authenticator
$passport = $authenticator->authenticate($request);
//...
} catch (AuthenticationException $e) {
// oh no! Authentication failed!
$response = $this->handleAuthenticationFailure($e, $request, $authenticator, $passport);
// ...
}
//...
private function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator, ?PassportInterface $passport): ?Response
{
// Avoid leaking error details in case of invalid user (e.g. user not found or invalid account status)
// to prevent user enumeration via response content comparison
if ($this->hideUserNotFoundExceptions && ($authenticationException instanceof UsernameNotFoundException || ($authenticationException instanceof AccountStatusException && !$authenticationException instanceof CustomUserMessageAccountStatusException))) {
$authenticationException = new BadCredentialsException('Bad credentials.', 0, $authenticationException);
}
$response = $authenticator->onAuthenticationFailure($request, $authenticationException);
//...
}
The default functionality of AbstractLoginFormAuthenticator::onAuthenticationFailure()
uses $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
which is what is adding the message from the Exception, that is retrieved by calling AuthenticationUtils::getLastAuthenticationError();
which calls $session->get(Security::AUTHENTICATION_ERROR);
public function getLastAuthenticationError(bool $clearSession = true)
{
$request = $this->getRequest();
$authenticationException = null;
//...
if ($request->hasSession() && ($session = $request->getSession())->has(Security::AUTHENTICATION_ERROR)) {
$authenticationException = $session->get(Security::AUTHENTICATION_ERROR);
//...
}
return $authenticationException;
}
How to display a Custom Error Message
There are various points in the application that are called where you can throw the Exception that gets passed to onAuthenticationFailure()
.
By default Symfony uses the security configuration settings to produce generic messages, but can be overridden by implementing them in your classes.
As the exception is overridden by the conditional mentioned above.
if ($this->hideUserNotFoundExceptions && !$e instanceof CustomUserMessageAccountStatusException) {
throw new BadCredentialsException('Bad credentials.', 0, $e);
}
You will need to disable hide_user_not_found
, otherwise the CustomUserMessageAuthenticationException
will be replaced with a BadCredentialsException('Bad credentials.')
exception.
Otherwise skip disabling hide_user_not_found
and throw the CustomUserMessageAccountStatusException
instead of the CustomUserMessageAuthenticationException
.
# /config/packages/security.yaml
security:
# ...
hide_user_not_found: false
firewalls:
# ...
In the Authenticator class
For an up-to-date usage reference, please read the default FormLoginAuthenticator
. Since the tutorials on the Symfony Casts site have not been updated for Symfony 5.
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
class MyLoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
//...
public function getCredentials(Request $request)
{
//...
throw new CustomUserMessageAuthenticationException('Custom Error');
}
public function authenticate(Request $request): PassportInterface
{
//general usage of how the other exceptions are thrown here
$credentials = $this->getCredentials($request);
$method = 'loadUserByIdentifier';
$badge = new UserBadge($credentials['username'], [$this->userProvider, $method]),
$user = $badge->getUser(); //UserRepository::loadUserByIdentifier()
//...
throw new CustomUserMessageAuthenticationException('Custom Error');
}
}
In the UserProvider class
class UserRepository extends ServiceEntityRepository implements UserProviderInterface
{
//....
public function loadUserByIdentifier(string $identifier)
{
throw new CustomUserMessageAuthenticationException('Custom Error');
//...
}
public function loadUserByUsername(string $username)
{
return $this->loadUserByIdentifier($username);
}
}
In the UserChecker class
use Symfony\Component\Security\Core\User\{UserCheckerInterface, UserInterface};
class UserChecker implements UserCheckerInterface
{
public function checkPreAuth(UserInterface $user): void
{
throw new CustomUserMessageAuthenticationException('Custom Error');
//...
}
public function checkPostAuth(UserInterface $user): void
{
throw new CustomUserMessageAuthenticationException('Custom Error');
//...
}
}