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-loginAsks 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
<?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
{% 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
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_loginlogin_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.