phpauthenticationsymfonysymfony6

Enable Symfony 6.3 login via username and via email at the same time


PROBLEM: It's easily possible to log in via username (and password) or to log in via email (and password) but how to enable both for an optimal user experience?

APPROACHES: I found multiple answers on Stackoverflow* and various tutorials, none worked for my Symfony version (6.3.3) or I was just to stupid to make them work (professional web developer).

SOLUTION: I actually found an elegant workaround. The reason why I'm still asking this question is a) to share my solution with others and b) maybe there's something wrong with it that I'm not seeing it and there's still a far better way of doing it?

*Similar questions, but with old versions of Symfony (and they didn't work for me):


Solution

  • So, my solution is:

    Instead of doing the following:

    1. login form
    2. redirects to login route, where the Symfony login magic happens
    3. redirects to login controller, where I can edit the result (redirect if successful, error message if not successful)

    I included another action between step 1 and step 2:

    1. login form:
    <form action="{{ path('pre_security_login') }}" method="post" class="d-flex">
        <input type="text" name="email_or_username" required="required" placeholder="email or username">
        <input type="password" name="_password" required="required" placeholder="password">
        <input type="submit" name="login" value="Login">
        <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
        <input type="hidden" name="_target_path" value="{{ app.request.get('redirect_to') }}">
    </form>
    

    1.5 redirects to my own Controller Action, not touching the Symfony login logic: Controller/AuthenticationController.php

    ...
    class AuthenticationController extends BaseController
    ...
        #[Route("/pre_login", name: "pre_security_login")]
        public function PreAuthenticationAction(Request $request, UserRepository $userRepository): Response
        {
            if ($this->getUser()) {
                return $this->redirectToRoute(HomeController::ROUTE_HOME);
            }
    
            $usernameOrEmail = $request->request->get('email_or_username');
            if (str_contains($usernameOrEmail, '@')) {
                $email = $usernameOrEmail;
                $username = $userRepository->findOneBy(['email ' => $email ])?->getUsername() ?? '';
            } else {
                $username = $usernameOrEmail;
            }
    
            $password = $request->request->get('_password');
            $token = $request->request->get('_csrf_token');
    
            return $this->redirectToRoute('security_login', [
                '_username' => $username,
                '_password' => $password,
                '_csrf_token' => $token,
            ]);
        }
    
    1. Now, the real login happens, with the Symfony logic. Note that this only works with the associated configuration (I'm using PHP, the default is YAML): config/packages/security.php
            'firewalls' => [
    ...
                'main' => [
    ...
                    'form_login' => [
                        'check_path' => 'security_login',
                        'login_path' => 'access_denied',
                        'form_only' => false, // <-- important, as it's no longer a form
                        'post_only' => false, // <-- important, as it's now a get request
                        'enable_csrf' => true,
                        'default_target_path' => 'home',
                    ],
                ],
            ],
    ...
    

    Step 3 remains like before:

    class AuthenticationController extends BaseController
    ...
        #[Route("/login", name: "security_login")]
        public function loginAction(AuthenticationUtils $authenticationUtils): Response
        {
            if ($this->getUser()) {
                return $this->redirectToRoute('home');
            }
    
            // get the login error if there is one
            $error = $authenticationUtils->getLastAuthenticationError();
            // last username entered by the user
            $lastUsername = $authenticationUtils->getLastUsername();
    
            return $this->render('authentication/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
        }