Login and access control
The Security component handles login, roles and protected sections in Symfony. Commands generate most of the code, and access rules are described in one file — config/packages/security.yaml.
The user
php bin/console make:user
php bin/console make:migration
php bin/console doctrine:migrations:migrateThe command creates a User entity with email, password and roles fields and registers it in the security settings.
Login form and registration
php bin/console make:security:form-login
php bin/console make:registration-formThe first command creates a controller, a login template and the /login and /logout routes; the second one a registration page.
Hashing the password
A password is never stored as plain text. A service hashes it:
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
public function register(UserPasswordHasherInterface $hasher, EntityManagerInterface $em): Response
{
$user = new User();
$user->setEmail('anna@example.com');
$user->setPassword($hasher->hashPassword($user, 'secret-password'));
$em->persist($user);
$em->flush();
// …
}Protected sections
Rules by address are written in security.yaml:
security:
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/profile, roles: ROLE_USER }Symfony sends a guest who opens /profile to the login page. A user without ROLE_ADMIN gets a 403 error on /admin.
Roles
A role is a string starting with ROLE_. Every logged-in user has ROLE_USER. Higher roles can include lower ones:
security:
role_hierarchy:
ROLE_ADMIN: ROLE_EDITOR
ROLE_EDITOR: ROLE_USERChecking in a controller
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route('/admin/posts', name: 'admin_posts')]
#[IsGranted('ROLE_ADMIN')]
public function posts(): Response
{
$user = $this->getUser();
// …
}The attribute can also go above the class — then it applies to every method. Inside a method the check is $this->denyAccessUnlessGranted('ROLE_EDITOR').
Checking in a template
{% if is_granted('ROLE_ADMIN') %}
<a href="{{ path('admin_posts') }}">Manage</a>
{% endif %}
{% if app.user %}
<p>Logged in as {{ app.user.email }}</p>
<a href="{{ path('app_logout') }}">Log out</a>
{% else %}
<a href="{{ path('app_login') }}">Log in</a>
{% endif %}Rights to a particular record
The rule "only the author may edit a post" goes into a separate class — a voter. make:voter creates it, and it is checked the same way:
$this->denyAccessUnlessGranted('POST_EDIT', $post);Common mistake
access_control rules are checked top to bottom, and the first match applies. If the general ^/ rule sits above ^/admin, the second one is never reached. Put exact addresses above general ones. And a link hidden in a template does not protect the page: access must be checked on the server.