Symfony: Login form on every page
I was working on a Symfony based project, where it should be possible to login from every page and, of course, come page to the original page after successful login. I couldn't find specific in the Symfony documentation about that, but I found several events around the authentication and the possibility to define the target URL after successful login. I combined that into the following solution.
Login form configuration from `security.yaml`:
security:
...
firewalls:
...
main:
...
form_login:
login_path: login
check_path: login
enable_csrf: true
Login form template including the CSRF token and the `_target_path`. The `_target_path` contains the following values, in order if available:
- `targetPath` variable, given by `SecurityController` from a previous login request
- URL to current URL (current route with current route parameters)
- URL to the home page
<form method="post" class="form" action="{{ path('login') }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
<input type="hidden" name="_target_path" value="{{ targetPath | default(path(app.current_route, app.current_route_parameters)) | default('home') }}">
...
</form>
I registered an event listener for the `LoginFailureEvent`, that reads the `_target_path` from the request and stores it in the session, to make i available for the `TargetPathTrait`, when displaying the login form again.
<?php
namespace MyVendor\MyPackage\EventListener;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
#[AsEventListener]
class SaveTargetUrlOnLoginFailureListener
{
public function __construct(protected Security $security)
{
}
public function __invoke(LoginFailureEvent $event)
{
$request = $event->getRequest();
$session = $request->getSession();
$firewallName = $this->security->getFirewallConfig($request)->getName();
$session->set('_security.' . $firewallName . '.target_path', $request->get('_target_path'));
}
}
Finally the `SecurityController`, that takes care of displaying the login page, uses the `TargetPathTrait` to provide stored target path from the session to the login form template, again.
<?php
namespace MyVendor\MyPackage\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class SecurityController extends AbstractController
{
use TargetPathTrait;
#[Route(path: '/login', name: 'login')]
public function login(Request $request, Session $session, AuthenticationUtils $authenticationUtils, Security $security): Response
{
// 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,
'targetPath' => $this->getTargetPath($session, $security->getFirewallConfig($request)->getName()),
]);
}
// ...
}
I extracted the login form template into a separate Twig template to be user as an include. This way, I can display the login form on any page without repeating myself.
I hope this helps you to solve your current problem. Feel free to comment, if this was helpful or if you have any questions.
This website uses Disqus for displaying comments. Due to privacy regulations you need to explicitly consent to loading comments from Disqus.
Show comments