Security Basics: Firewall and Provider
Security Basics: Firewall and Provider
~16 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
So far, EVERY page of our task manager was reachable by ANYONE – time to set up the security bundle and build the full User entity.
Installing the security bundle
composer require symfony/security-bundleThe complete User entity
php bin/console make:usermake:user interactively asks for the entity name, identifier field (email or username), and whether passwords should be hashed – extends the User entity known from chapter 23 with TWO required interfaces:
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity(repositoryClass: UserRepository::class)]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 180, unique: true)]
private string $email = '';
#[ORM\Column(length: 180)]
private string $name = '';
/**
* @var list<string>
*/
#[ORM\Column]
private array $roles = [];
#[ORM\Column]
private string $password = '';
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): static
{
$this->email = $email;
return $this;
}
public function getUserIdentifier(): string
{
return $this->email;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
/**
* @return list<string>
*/
public function getRoles(): array
{
$roles = $this->roles;
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
/**
* @param list<string> $roles
*/
public function setRoles(array $roles): static
{
$this->roles = $roles;
return $this;
}
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): static
{
$this->password = $password;
return $this;
}
public function eraseCredentials(): void
{
// Clear a plain-text password here (if temporarily held on the object)
}
}The four required methods in detail
getUserIdentifier()– the unique login identifier (here: email), not necessarily the ID.getRoles()– ALWAYS return AT LEAST['ROLE_USER'], since Symfony otherwise treats some security checks as "no access" instead of "unknown role".getPassword()– the HASHED password (chapter 28), NEVER plain text.eraseCredentials()– called after authentication to clear sensitive temporary data (usually empty in our simple setup).
Understanding config/packages/security.yaml
security:
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
access_control:
- { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/projects, roles: ROLE_USER }Provider vs. firewall: the decisive difference
| Concept | Purpose |
|---|---|
| Provider | Answers: "WHERE does user data come from?" – here: from the User entity, looked up by email. |
| Firewall | Answers: "WHICH URL area even needs authentication, and HOW (form, token, ...)?" – the dev firewall entry, for instance, excludes the debug toolbar from ANY security check. |
access_control defines WHICH role is needed for WHICH URL pattern – PUBLIC_ACCESS for the login page itself (otherwise NOBODY could log in), ROLE_USER for /projects. Rules get checked TOP to BOTTOM, the FIRST match wins – EXACTLY like routing (chapter 7).
Migration for the extended User entity
php bin/console make:migration
php bin/console doctrine:migrations:migrateTipp: Current state: Symfony now KNOWS how to find and check users – but there's still NO way to actually log in. Chapter 27 builds exactly that.