WEBcoast Logo

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.