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):
So, my solution is:
Instead of doing the following:
I included another action between step 1 and step 2:
<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,
]);
}
'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]);
}