Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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:migrate

The 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-form

The 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_USER

Checking 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.

Thanks for reading!

Donate to the site — we work hard for you.