Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Implementing a Login Form

Implementing a Login Form

~15 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

With the skeleton from chapter 26, let's now build the actual login authenticator – Symfony's built-in form_login mechanism.

Generating the login authenticator

php bin/console make:security:form-login

Asks for the controller name, route names for login/logout, and whether to redirect to a fixed page after a successful login. Generates a controller AND automatically adjusts security.yaml.

The generated login controller

src/Controller/SecurityController.php
<?php

declare(strict_types=1);

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class SecurityController extends AbstractController
{
    #[Route('/login', name: 'app_login')]
    public function login(AuthenticationUtils $authenticationUtils): Response
    {
        $error = $authenticationUtils->getLastAuthenticationError();
        $lastEmail = $authenticationUtils->getLastUsername();

        return $this->render('security/login.html.twig', [
            'last_username' => $lastEmail,
            'error' => $error,
        ]);
    }

    #[Route('/logout', name: 'app_logout')]
    public function logout(): void
    {
        throw new \LogicException('Intercepted automatically by the security system.');
    }
}

logout() NEVER actually runs – Symfony's security system intercepts the /logout request BEFORE the controller (configured in security.yaml). The exception serves as a clear signal: "if this code ever executes, something is misconfigured".

The login template

templates/security/login.html.twig
{% extends 'base.html.twig' %}

{% block body %}
    <h1>Log In</h1>

    {% if error %}
        <div class="alert alert-error">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
    {% endif %}

    <form method="post">
        <label for="email">Email</label>
        <input type="email" id="email" name="email" value="{{ last_username }}" required autofocus>

        <label for="password">Password</label>
        <input type="password" id="password" name="password" required>

        <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">

        <button type="submit">Log In</button>
    </form>
{% endblock %}

IMPORTANT: this form is NOT a Symfony Form (chapter 15) – form_login expects a CLASSIC HTML form with the field name attributes email/password, processed directly by the firewall, NOT by the controller. That's why the CSRF token (chapter 18) is inserted MANUALLY via csrf_token('authenticate').

security.yaml: configuring form_login

config/packages/security.yaml
security:
    # ... providers as in chapter 26 ...

    firewalls:
        main:
            lazy: true
            provider: app_user_provider
            form_login:
                login_path: app_login
                check_path: app_login
                enable_csrf: true
            logout:
                path: app_logout
                target: app_login

login_path DISPLAYS the form, check_path PROCESSES the POST request – usually the SAME route (both GET AND POST on /login, EXACTLY the form pattern from chapter 9). enable_csrf: true enables checking the _csrf_token field from above.

Login/logout in navigation

<nav>
    <a href="{{ path('project_index') }}">Projects</a>

    {% if app.user %}
        <span>Logged in as {{ app.user.name }}</span>
        <a href="{{ path('app_logout') }}">Log Out</a>
    {% else %}
        <a href="{{ path('app_login') }}">Log In</a>
    {% endif %}
</nav>

app.user (already known from chapter 13) is NOW actually populated – null for non-logged-in visitors, otherwise the logged-in user's User entity.

Tipp: A login attempt currently ALWAYS fails, since no user with a correctly hashed password exists yet – chapter 28 builds registration, which fixes that.