PHP Logosymfony/security-bundle

The `symfony/security-bundle` is a fundamental component of the Symfony framework, providing a robust and flexible security layer for web applications. It handles various aspects of application security, including authentication, authorization, user management, password hashing, CSRF protection, and more.

Core Concepts:

1. Users: Represents the entity trying to access the application. Users typically implement `Symfony\Component\Security\Core\User\UserInterface` and contain properties like username, password, roles, and a salt (though salt is often integrated into modern password hashing algorithms).

2. User Providers: Services responsible for loading user information from a data source (e.g., database, LDAP, in-memory array) based on credentials (like a username). They implement `Symfony\Component\Security\Core\User\UserProviderInterface`.

3. Authentication: The process of verifying a user's identity. The Security Bundle allows for various authentication methods (e.g., form login, HTTP basic, API tokens, OAuth, SAML) via `Authenticators`. When a user tries to access a protected resource, an authenticator attempts to verify their credentials.

4. Firewalls: The central entry point for security. A firewall defines a security context for a specific URL pattern. It intercepts requests, determines which authentication methods to use, and manages user sessions. You can have multiple firewalls, each configured for different parts of your application.

5. Authorization: The process of determining what an authenticated user is allowed to do. This is managed through:
* Roles: Permissions assigned to users (e.g., `ROLE_USER`, `ROLE_ADMIN`).
* Access Control: Rules defined in `security.yaml` that specify which roles are required to access certain URL patterns or methods.
* Voters: Custom logic for complex authorization decisions that go beyond simple role checks. Voters implement `Symfony\Component\Security\Core\Authorization\Voter\VoterInterface`.
* `isGranted()`: A method available in controllers and Twig templates to check if the current user has a specific role or permission.
* `@IsGranted` attribute: An annotation (or attribute in PHP 8+) that can be placed on controller methods to automatically deny access if the user doesn't meet the specified security criteria.

6. Password Hashing: Securely stores user passwords by hashing them with strong algorithms (e.g., Argon2i, Bcrypt). The bundle abstracts away the complexity of choosing and implementing hashing algorithms.

7. CSRF Protection: Provides protection against Cross-Site Request Forgery attacks, typically by embedding a unique token in forms and verifying it upon submission.

8. Remember Me: Allows users to stay logged in across browser sessions without re-entering credentials.

Configuration (`security.yaml`):

Most of the `symfony/security-bundle` configuration resides in the `config/packages/security.yaml` file. Here, you define:
* `providers`: How users are loaded.
* `firewalls`: Which URLs are protected and how (e.g., form_login, logout).
* `access_control`: Specific authorization rules based on URL patterns and roles.
* `encoders` (or `password_hashers` in newer versions): How passwords are hashed.

In essence, the security bundle provides a powerful and extensible framework for securing virtually any type of Symfony application, making it a cornerstone for building robust and safe web services.

Example Code

```php
<?php

// src/Entity/User.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private ?int $id = null;

    #[ORM\Column(type: 'string', length: 180, unique: true)]
    private ?string $email = null;

    #[ORM\Column(type: 'json')]
    private array $roles = [];

    #[ORM\Column(type: 'string')]
    private ?string $password = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function setEmail(string $email): self
    {
        $this->email = $email;

        return $this;
    }

    /
     * A visual identifier that represents this user.
     *
     * @see UserInterface
     */
    public function getUserIdentifier(): string
    {
        return (string) $this->email;
    }

    /
     * @deprecated since Symfony 5.3, use getUserIdentifier instead
     */
    public function getUsername(): string
    {
        return $this->getUserIdentifier();
    }

    /
     * @see UserInterface
     */
    public function getRoles(): array
    {
        $roles = $this->roles;
        // guarantee every user at least has ROLE_USER
        $roles[] = 'ROLE_USER';

        return array_unique($roles);
    }

    public function setRoles(array $roles): self
    {
        $this->roles = $roles;

        return $this;
    }

    /
     * @see PasswordAuthenticatedUserInterface
     */
    public function getPassword(): string
    {
        return $this->password;
    }

    public function setPassword(string $password): self
    {
        $this->password = $password;

        return $this;
    }

    /
     * Returning a salt is only needed, if you are not using a modern
     * hashing algorithm (e.g. bcrypt or argon2i).
     *
     * @see UserInterface
     */
    public function getSalt(): ?string
    {
        return null;
    }

    /
     * @see UserInterface
     */
    public function eraseCredentials()
    {
        // If you store any temporary, sensitive data on the user, clear it here
        // $this->plainPassword = null;
    }
}

// src/Controller/SecurityController.php
namespace App\Controller;

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

class SecurityController extends AbstractController
{
    #[Route('/login', name: 'app_login')]
    public function login(AuthenticationUtils $authenticationUtils): Response
    {
        // get the login error if there is one
        $error = $authenticationUtils->getLastAuthenticationError();
        // last username entered by the user
        $lastUsername = $authenticationUtils->getLastUsername();

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

    #[Route('/logout', name: 'app_logout', methods: ['GET'])]
    public function logout(): never
    {
        // controller can be blank: it will never be executed!
        // The logout happens in your security.yaml configuration.
        throw new \Exception('Don\'t forget to activate logout in security.yaml');
    }

    #[Route('/', name: 'app_homepage')]
    public function homepage(): Response
    {
        return $this->render('security/homepage.html.twig', [
            'message' => 'Welcome to the public homepage!',
        ]);
    }

    /
     * Requires the user to be logged in.
     */
    #[Route('/dashboard', name: 'app_dashboard')]
    public function dashboard(): Response
    {
        // The user is automatically available via $this->getUser()
        $user = $this->getUser();

        if (!$user) {
            // This block is technically unreachable if a firewall is configured properly
            // to redirect unauthenticated users, but good for type safety.
            throw $this->createAccessDeniedException('You must be logged in to access the dashboard.');
        }

        return $this->render('security/dashboard.html.twig', [
            'message' => 'Welcome to your dashboard, ' . $user->getUserIdentifier() . '!',
            'user' => $user,
        ]);
    }

    /
     * Requires the user to have ROLE_ADMIN.
     * The #[IsGranted] attribute automatically denies access if the user doesn't have the role.
     */
    #[Route('/admin', name: 'app_admin_panel')]
    #[IsGranted('ROLE_ADMIN', message: 'You do not have administrative access.')]
    public function adminPanel(): Response
    {
        return $this->render('security/admin_panel.html.twig', [
            'message' => 'Welcome to the admin panel! Only accessible by admins.',
        ]);
    }

    /
     * Alternative way to check roles inside the controller logic.
     */
    #[Route('/moderator', name: 'app_moderator_panel')]
    public function moderatorPanel(): Response
    {
        if (!$this->isGranted('ROLE_MODERATOR')) {
            throw $this->createAccessDeniedException('You do not have moderator access.');
        }

        return $this->render('security/moderator_panel.html.twig', [
            'message' => 'Welcome to the moderator panel!',
        ]);
    }
}

// config/packages/security.yaml (simplified example, actual config might be more extensive)
# security:
#    password_hashers:
#        App\Entity\User: 'auto'
#
#    providers:
#        app_user_provider:
#            entity:
#                class: App\Entity\User
#                property: email
#
#    firewalls:
#        dev:
#            pattern: ^/(_(profiler|wdt)|css|images|js)/ # Exclude common assets from security
#            security: false
#
#        main:
#            lazy: true
#            provider: app_user_provider
#            form_login:
#                login_path: app_login
#                check_path: app_login # The URL to submit the login form to
#                target_path_parameter: _target_path
#            logout:
#                path: app_logout
#                target: app_homepage
#
#    access_control:
#        - { path: ^/login, roles: PUBLIC_ACCESS }
#        - { path: ^/dashboard, roles: ROLE_USER }
#        - { path: ^/admin, roles: ROLE_ADMIN }
#        - { path: ^/moderator, roles: ROLE_MODERATOR }

// templates/security/login.html.twig (simple login form)
{# templates/security/login.html.twig #}
{% extends 'base.html.twig' %}

{% block title %}Log in!{% endblock %}

{% block body %}
    <form action="{{ path('app_login') }}" method="post">
        {% if error %}
            <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
        {% endif %}

        <label for="username">Email:</label>
        <input type="text" id="username" name="_username" value="{{ last_username }}" />

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

        {# A CSRF token is recommended for form submissions #}
        <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">

        <button type="submit">Login</button>
    </form>
{% endblock %}
```