Skip to content
Advertisement

Customize new authentication errors messages on Symfony5.3

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 SymfonyComponentSecurityHttpAuthenticator;

use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentSecurityCoreSecurity;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentHttpFoundationRedirectResponse;
use SymfonyComponentSecurityCoreExceptionAuthenticationException;
use SymfonyComponentSecurityHttpEntryPointAuthenticationEntryPointInterface;
use SymfonyComponentSecurityCoreExceptionCustomUserMessageAuthenticationException;

/**
 * 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 AppController;

use AppEntityUser;
use DateTimeImmutable;
use AppFormRegistrationType;
use DoctrineORMEntityManagerInterface;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingAnnotationRoute;
use SymfonyBundleFrameworkBundleControllerAbstractController;
use SymfonyComponentSecurityHttpAuthenticationAuthenticationUtils;
use SymfonyComponentPasswordHasherHasherUserPasswordHasherInterface;


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" class="form-control" autocomplete="email" required autofocus>
    <label for="inputPassword">Password</label>
    <input type="password" name="password" id="inputPassword" class="form-control" 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 class="checkbox mb-3">
            <label>
                <input type="checkbox" name="_remember_me"> Remember me
            </label>
        </div>
    #}

    <button class="btn btn-lg btn-primary" type="submit">
        Sign in
    </button>
</form>
{% endblock %}

Advertisement

Answer

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 SymfonyComponentSecurityHttpAuthenticatorAbstractLoginFormAuthenticator;

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 SymfonyComponentSecurityCoreUser{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');

        //...
    }
}
User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement