Files
archived-symfony-docs/security.rst
Oskar Stark 70ae196be4 Merge branch '6.4' into 7.3
* 6.4:
  Update DOCtor-RST to 1.77.0 and enable `no_relative_doc_path` rule
2025-12-10 15:42:23 +01:00

3026 lines
104 KiB
ReStructuredText

Security
========
Symfony provides many tools to secure your application. Some HTTP-related
security tools, like :doc:`secure session cookies </session>` and
:doc:`CSRF protection </security/csrf>` are provided by default. The
SecurityBundle, which you will learn about in this guide, provides all
authentication and authorization features needed to secure your
application.
.. _security-installation:
To get started, install the SecurityBundle:
.. code-block:: terminal
$ composer require symfony/security-bundle
If you have :ref:`Symfony Flex <symfony-flex>` installed, this also
creates a ``security.yaml`` configuration file for you:
.. code-block:: yaml
# config/packages/security.yaml
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
users_in_memory: { memory: null }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users_in_memory
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# An easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
That's a lot of config! In the next sections, the three main elements are
discussed:
`The User`_ (``providers``)
Any secured section of your application needs some concept of
a user. The user provider loads users from any storage (e.g. the
database) based on a "user identifier" (e.g. the user's email address);
`The Firewall`_ & `Authenticating Users`_ (``firewalls``)
The firewall is the core of securing your application. Every request
within the firewall is checked if it needs an authenticated user. The
firewall also takes care of authenticating this user (e.g. using a
login form);
`Access Control (Authorization)`_ (``access_control``)
Using access control and the authorization checker, you control the
required permissions to perform a specific action or visit a specific
URL.
.. _create-user-class:
.. _a-create-your-user-class:
The User
--------
Permissions in Symfony are always linked to a user object. If you need to
secure (parts of) your application, you need to create a user class. This
is a class that implements :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`.
This is often a Doctrine entity, but you can also use a dedicated
Security user class.
The easiest way to generate a user class is using the ``make:user`` command
from the `MakerBundle`_:
.. code-block:: terminal
$ php bin/console make:user
The name of the security user class (e.g. User) [User]:
> User
Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]:
> yes
Enter a property name that will be the unique "display" name for the user (e.g. email, username, uuid) [email]:
> email
Will this app need to hash/check user passwords? Choose No if passwords are not needed or will be checked/hashed by some other system (e.g. a single sign-on server).
Does this app need to hash/check user passwords? (yes/no) [yes]:
> yes
created: src/Entity/User.php
created: src/Repository/UserRepository.php
updated: src/Entity/User.php
updated: config/packages/security.yaml
.. code-block:: php
// src/Entity/User.php
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(type: 'integer')]
private int $id;
#[ORM\Column(type: 'string', length: 180, unique: true)]
private ?string $email;
#[ORM\Column(type: 'json')]
private array $roles = [];
#[ORM\Column(type: 'string')]
private string $password;
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;
}
/**
* The public representation of the user (e.g. a username, an email address, etc.)
*
* @see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->email;
}
/**
* @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;
}
// [...]
}
.. tip::
Starting in `MakerBundle`_: v1.57.0 - You can pass either ``--with-uuid`` or
``--with-ulid`` to ``make:user``. Leveraging Symfony's :doc:`Uid Component </components/uid>`,
this generates a ``User`` entity with the ``id`` type as :ref:`Uuid <uuid>`
or :ref:`Ulid <ulid>` instead of ``int``.
If your user is a Doctrine entity, like in the example above, don't forget
to create the tables by :ref:`creating and running a migration <doctrine-creating-the-database-tables-schema>`:
.. code-block:: terminal
$ php bin/console make:migration
$ php bin/console doctrine:migrations:migrate
.. tip::
Starting in `MakerBundle`_: v1.56.0 - Passing ``--formatted`` to ``make:migration``
generates a nice and tidy migration file.
.. _where-do-users-come-from-user-providers:
.. _security-user-providers:
Loading the User: The User Provider
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Besides creating the entity, the ``make:user`` command also adds config
for a user provider in your security configuration:
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
# ...
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<provider name="app_user_provider">
<entity class="App\Entity\User" property="email"/>
</provider>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use App\Entity\User;
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
// ...
$security->provider('app_user_provider')
->entity()
->class(User::class)
->property('email')
;
};
This user provider knows how to (re)load users from a storage (e.g. a database)
based on a "user identifier" (e.g. the user's email address or username).
The configuration above uses Doctrine to load the ``User`` entity using the
``email`` property as "user identifier".
User providers are used in a couple places during the security lifecycle:
**Load the User based on an identifier**
During login (or any other authenticator), the provider loads the user
based on the user identifier. Some other features, like
:doc:`user impersonation </security/impersonating_user>` and
:doc:`Remember Me </security/remember_me>` also use this.
**Reload the User from the session**
At the beginning of each request, the user is loaded from the
session (unless your firewall is ``stateless``). The provider
"refreshes" the user (e.g. the database is queried again for fresh
data) to make sure all user information is up to date (and if
necessary, the user is de-authenticated/logged out if something
changed). See :ref:`user_session_refresh` for more information about
this process.
Symfony comes with several built-in user providers:
:ref:`Entity User Provider <security-entity-user-provider>`
Loads users from a database using :doc:`Doctrine </doctrine>`;
:ref:`LDAP User Provider <security-ldap-user-provider>`
Loads users from a LDAP server;
:ref:`Memory User Provider <security-memory-user-provider>`
Loads users from a configuration file;
:ref:`Chain User Provider <security-chain-user-provider>`
Merges two or more user providers into a new user provider.
Since each firewall has exactly *one* user provider, you can use this
to chain multiple providers together.
The built-in user providers cover the most common needs for applications, but you
can also create your own :ref:`custom user provider <security-custom-user-provider>`.
.. note::
Sometimes, you need to inject the user provider in another class (e.g.
in your custom authenticator). All user providers follow this pattern
for their service ID: ``security.user.provider.concrete.<your-provider-name>``
(where ``<your-provider-name>`` is the configuration key, e.g.
``app_user_provider``). If you only have one user provider, you can autowire
it using the :class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`
type-hint.
.. _security-encoding-user-password:
Registering the User: Hashing Passwords
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Many applications require a user to log in with a password. For these
applications, the SecurityBundle provides password hashing and verification
functionality.
First, make sure your User class implements the
:class:`Symfony\\Component\\Security\\Core\\User\\PasswordAuthenticatedUserInterface`::
// src/Entity/User.php
// ...
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
// ...
/**
* @return string the hashed password for this user
*/
public function getPassword(): string
{
return $this->password;
}
}
Then, configure which password hasher should be used for this class. If your
``security.yaml`` file wasn't already pre-configured, then ``make:user`` should
have done this for you:
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
# ...
password_hashers:
# Use native password hasher, which auto-selects and migrates the best
# possible hashing algorithm (which currently is "bcrypt")
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<!-- Use native password hasher, which auto-selects and migrates the best
possible hashing algorithm (currently this is "bcrypt") -->
<password-hasher class="Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface" algorithm="auto"/>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use App\Entity\User;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
return static function (SecurityConfig $security): void {
// ...
// Use native password hasher, which auto-selects and migrates the best
// possible hashing algorithm (currently this is "bcrypt")
$security->passwordHasher(PasswordAuthenticatedUserInterface::class)
->algorithm('auto')
;
};
Now that Symfony knows *how* you want to hash the passwords, you can use the
``UserPasswordHasherInterface`` service to do this before saving your users to
the database::
// src/Controller/RegistrationController.php
namespace App\Controller;
// ...
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class RegistrationController extends AbstractController
{
public function index(UserPasswordHasherInterface $passwordHasher): Response
{
// ... e.g. get the user data from a registration form
$user = new User(...);
$plaintextPassword = ...;
// hash the password (based on the security.yaml config for the $user class)
$hashedPassword = $passwordHasher->hashPassword(
$user,
$plaintextPassword
);
$user->setPassword($hashedPassword);
// ...
}
}
.. note::
If your user class is a Doctrine entity and you hash user passwords, the
Doctrine repository class related to the user class must implement the
:class:`Symfony\\Component\\Security\\Core\\User\\PasswordUpgraderInterface`.
.. _security-make-registration-form:
.. tip::
The ``make:registration-form`` maker command can help you set-up the
registration controller and add features like email address
verification using the `SymfonyCastsVerifyEmailBundle`_.
.. code-block:: terminal
$ composer require symfonycasts/verify-email-bundle
$ php bin/console make:registration-form
You can also manually hash a password by running:
.. code-block:: terminal
$ php bin/console security:hash-password
Read more about all available hashers and password migration in
:doc:`/security/passwords`.
.. _firewalls-authentication:
.. _a-authentication-firewalls:
The Firewall
------------
The ``firewalls`` section of ``config/packages/security.yaml`` is the *most*
important section. A "firewall" is your authentication system: the firewall
defines which parts of your application are secured and *how* your users
will be able to authenticate (e.g. login form, API token, etc).
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
# ...
firewalls:
# the order in which firewalls are defined is very important, as the
# request will be handled by the first firewall whose pattern matches
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
# a firewall with no pattern should be defined last because it will match all requests
main:
lazy: true
# provider that you set earlier inside providers
provider: app_user_provider
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<!-- the order in which firewalls are defined is very important, as the
request will be handled by the first firewall whose pattern matches -->
<firewall name="dev"
pattern="^/(_(profiler|wdt)|css|images|js)/"
security="false"/>
<!-- a firewall with no pattern should be defined last because it will match all requests -->
<firewall name="main"
lazy="true"/>
<!-- activate different ways to authenticate
https://symfony.com/doc/current/security.html#firewalls-authentication -->
<!-- https://symfony.com/doc/current/security/impersonating_user.html -->
<!-- <switch-user/> -->
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
// ...
// the order in which firewalls are defined is very important, as the
// request will be handled by the first firewall whose pattern matches
$security->firewall('dev')
->pattern('^/(_(profiler|wdt)|css|images|js)/')
->security(false)
;
// a firewall with no pattern should be defined last because it will match all requests
$security->firewall('main')
->lazy(true)
// activate different ways to authenticate
// https://symfony.com/doc/current/security.html#firewalls-authentication
// https://symfony.com/doc/current/security/impersonating_user.html
// ->switchUser(true)
;
};
Only one firewall is active on each request: Symfony uses the ``pattern`` key
to find the first match (you can also
:doc:`match by host or other things </security/firewall_restriction>`).
Here, all real URLs are handled by the ``main`` firewall (no ``pattern`` key means
it matches *all* URLs).
The ``dev`` firewall is really a fake firewall: it makes sure that you
don't accidentally block Symfony's dev tools - which live under URLs like
``/_profiler`` and ``/_wdt``.
.. tip::
When matching several routes, instead of creating a long regex you can also
use an array of simpler regexes to match each route:
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
# ...
firewalls:
dev:
pattern:
- ^/_profiler/
- ^/_wdt/
- ^/css/
- ^/images/
- ^/js/
# ...
.. code-block:: php
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
// ...
$security->firewall('dev')
->pattern([
'^/_profiler/',
'^/_wdt/',
'^/css/',
'^/images/',
'^/js/',
])
->security(false)
;
// ...
};
This feature is not supported by the XML configuration format.
A firewall can have many modes of authentication, in other words, it enables many
ways to ask the question "Who are you?". Often, the user is unknown (i.e. not logged in)
when they first visit your website. If you visit your homepage right now, you *will*
have access and you'll see that you're visiting a page behind the firewall in the toolbar:
.. image:: /_images/security/anonymous_wdt.png
:alt: The Symfony profiler toolbar where the Security information shows "Authenticated: no" and "Firewall name: main"
Visiting a URL under a firewall doesn't necessarily require you to be authenticated
(e.g. the login form has to be accessible or some parts of your application
are public). On the other hand, all pages that you want to be *aware* of a logged in
user have to be under the same firewall. So if you want to display a *"You are logged in
as ..."* message on every page, they all have to be included in the same firewall.
You'll learn how to restrict access to URLs, controllers or
anything else within your firewall in the :ref:`access control
<security-access-control>` section.
.. tip::
The ``lazy`` anonymous mode prevents the session from being started if
there is no need for authorization (i.e. explicit check for a user
privilege). This is important to keep requests cacheable (see
:doc:`/http_cache`).
.. note::
If you do not see the toolbar, install the :doc:`profiler </profiler>`
with:
.. code-block:: terminal
$ composer require --dev symfony/profiler-pack
Fetching the Firewall Configuration for a Request
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you need to get the configuration of the firewall that matched a given request,
use the :class:`Symfony\\Bundle\\SecurityBundle\\Security` service::
// src/Service/ExampleService.php
// ...
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RequestStack;
class ExampleService
{
public function __construct(
// Avoid calling getFirewallConfig() in the constructor: auth may not
// be complete yet. Instead, store the entire Security object.
private Security $security,
private RequestStack $requestStack,
) {
}
public function someMethod(): void
{
$request = $this->requestStack->getCurrentRequest();
$firewallName = $this->security->getFirewallConfig($request)?->getName();
// ...
}
}
.. _security-authenticators:
Authenticating Users
--------------------
During authentication, the system tries to find a matching user for the
visitor of the webpage. Traditionally, this was done using a login form or
a HTTP basic dialog in the browser. However, the SecurityBundle comes with
many other authenticators:
* `Form Login`_
* `JSON Login`_
* `HTTP Basic`_
* `Login Link`_
* `X.509 Client Certificates`_
* `Remote users`_
* :doc:`Custom Authenticators </security/custom_authenticator>`
.. tip::
If your application logs users in via a third-party service such as
Google, Facebook or Twitter (social login), check out the `HWIOAuthBundle`_
community bundle or `Oauth2-client`_ package.
.. _security-form-login:
Form Login
~~~~~~~~~~
Most websites have a login form where users authenticate using an
identifier (e.g. email address or username) and a password. This
functionality is provided by the built-in :class:`Symfony\\Component\\Security\\Http\\Authenticator\\FormLoginAuthenticator`.
You can run the following command to create everything needed to add a login
form in your application:
.. code-block:: terminal
$ php bin/console make:security:form-login
This command will create the required controller and template and it will also
update the security configuration. Alternatively, if you prefer to make these
changes manually, follow the next steps.
First, create a controller for the login form:
.. code-block:: terminal
$ php bin/console make:controller Login
created: src/Controller/LoginController.php
created: templates/login/index.html.twig
.. code-block:: php
// src/Controller/LoginController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class LoginController extends AbstractController
{
#[Route('/login', name: 'app_login')]
public function index(): Response
{
return $this->render('login/index.html.twig', [
'controller_name' => 'LoginController',
]);
}
}
Then, enable the ``FormLoginAuthenticator`` using the ``form_login`` setting:
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
# ...
firewalls:
main:
# ...
form_login:
# "app_login" is the name of the route created previously
login_path: app_login
check_path: app_login
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:srv="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<firewall name="main">
<!-- "app_login" is the name of the route created previously -->
<form-login login-path="app_login" check-path="app_login"/>
</firewall>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
// ...
$mainFirewall = $security->firewall('main');
// "app_login" is the name of the route created previously
$mainFirewall->formLogin()
->loginPath('app_login')
->checkPath('app_login')
;
};
.. note::
The ``login_path`` and ``check_path`` support URLs and route names (but
cannot have mandatory wildcards - e.g. ``/login/{foo}`` where ``foo``
has no default value).
Once enabled, the security system redirects unauthenticated visitors to the
``login_path`` when they try to access a secured place (this behavior can
be customized using :ref:`authentication entry points <security-entry-point>`).
Edit the login controller to render the login form:
.. code-block:: diff
// ...
+ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class LoginController extends AbstractController
{
#[Route('/login', name: 'app_login')]
- public function index(): Response
+ public function index(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('login/index.html.twig', [
- 'controller_name' => 'LoginController',
+ 'last_username' => $lastUsername,
+ 'error' => $error,
]);
}
}
Don't let this controller confuse you. Its job is only to *render* the form.
The ``FormLoginAuthenticator`` will handle the form *submission* automatically.
If the user submits an invalid email or password, that authenticator will store
the error and redirect back to this controller, where we read the error (using
``AuthenticationUtils``) so that it can be displayed back to the user.
Finally, create or update the template:
.. code-block:: html+twig
{# templates/login/index.html.twig #}
{% extends 'base.html.twig' %}
{# ... #}
{% block body %}
{% if error %}
<div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
<form action="{{ path('app_login') }}" method="post">
<label for="username">Email:</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" required>
<label for="password">Password:</label>
<input type="password" id="password" name="_password" required>
{# If you want to control the URL the user is redirected to on success
<input type="hidden" name="_target_path" value="/account"> #}
<button type="submit">login</button>
</form>
{% endblock %}
.. warning::
The ``error`` variable passed into the template is an instance
of :class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException`.
It may contain sensitive information about the authentication failure.
*Never* use ``error.message``: use the ``messageKey`` property instead,
as shown in the example. This message is always safe to display.
The form can look like anything, but it usually follows some conventions:
* The ``<form>`` element sends a ``POST`` request to the ``app_login`` route, since
that's what you configured as the ``check_path`` under the ``form_login`` key in
``security.yaml``;
* The username (or whatever your user's "identifier" is, like an email) field has
the name ``_username`` and the password field has the name ``_password``.
.. tip::
Actually, all of this can be configured under the ``form_login`` key. See
:ref:`reference-security-firewall-form-login` for more details.
.. danger::
This login form is currently not protected against CSRF attacks. Read
:ref:`form_login-csrf` on how to protect your login form.
And that's it! When you submit the form, the security system automatically
reads the ``_username`` and ``_password`` POST parameter, loads the user via
the user provider, checks the user's credentials and either authenticates the
user or sends them back to the login form where the error can be displayed.
To review the whole process:
#. The user tries to access a resource that is protected (e.g. ``/admin``);
#. The firewall initiates the authentication process by redirecting the
user to the login form (``/login``);
#. The ``/login`` page renders login form via the route and controller created
in this example;
#. The user submits the login form to ``/login``;
#. The security system (i.e. the ``FormLoginAuthenticator``) intercepts the
request, checks the user's submitted credentials, authenticates the user if
they are correct, and sends the user back to the login form if they are not.
.. seealso::
You can customize the responses on a successful or failed login
attempt. See :doc:`/security/form_login`.
.. _form_login-csrf:
CSRF Protection in Login Forms
..............................
`Login CSRF attacks`_ can be prevented using the same technique of adding hidden
CSRF tokens into the login forms. The Security component already provides CSRF
protection, but you need to configure some options before using it.
First, you need to enable CSRF on the form login:
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
# ...
firewalls:
secured_area:
# ...
form_login:
# ...
enable_csrf: true
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<firewall name="secured_area">
<!-- ... -->
<form-login enable-csrf="true"/>
</firewall>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
// ...
$mainFirewall = $security->firewall('main');
$mainFirewall->formLogin()
// ...
->enableCsrf(true)
;
};
.. _csrf-login-template:
Then, use the ``csrf_token()`` function in the Twig template to generate a CSRF
token and store it as a hidden field of the form. By default, the HTML field
must be called ``_csrf_token`` and the string used to generate the value must
be ``authenticate``:
.. code-block:: html+twig
{# templates/login/index.html.twig #}
{# ... #}
<form action="{{ path('app_login') }}" method="post">
{# ... the login fields #}
<input type="hidden" name="_csrf_token" data-controller="csrf-protection" value="{{ csrf_token('authenticate') }}">
<button type="submit">login</button>
</form>
After this, you have protected your login form against CSRF attacks.
.. tip::
You can change the name of the field by setting ``csrf_parameter`` and change
the token ID by setting ``csrf_token_id`` in your configuration. See
:ref:`reference-security-firewall-form-login` for more details.
.. _security-json-login:
JSON Login
~~~~~~~~~~
Some applications provide an API that is secured using tokens. These
applications may use an endpoint that provides these tokens based on a
username (or email) and password. The JSON login authenticator helps you create
this functionality.
Enable the authenticator using the ``json_login`` setting:
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
# ...
firewalls:
main:
# ...
json_login:
# api_login is a route we will create below
check_path: api_login
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:srv="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<firewall name="main">
<json-login check-path="api_login"/>
</firewall>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
// ...
$mainFirewall = $security->firewall('main');
$mainFirewall->jsonLogin()
->checkPath('api_login')
;
};
.. note::
The ``check_path`` supports URLs and route names (but cannot have
mandatory wildcards - e.g. ``/login/{foo}`` where ``foo`` has no
default value).
The authenticator runs when a client requests the ``check_path``. First,
create a controller for this path:
.. code-block:: terminal
$ php bin/console make:controller --no-template ApiLogin
created: src/Controller/ApiLoginController.php
.. code-block:: php
// src/Controller/ApiLoginController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ApiLoginController extends AbstractController
{
#[Route('/api/login', name: 'api_login')]
public function index(): Response
{
return $this->json([
'message' => 'Welcome to your new controller!',
'path' => 'src/Controller/ApiLoginController.php',
]);
}
}
This login controller will be called after the authenticator successfully
authenticates the user. You can get the authenticated user, generate a
token (or whatever you need to return) and return the JSON response:
.. code-block:: diff
// ...
+ use App\Entity\User;
+ use Symfony\Component\Security\Http\Attribute\CurrentUser;
class ApiLoginController extends AbstractController
{
- #[Route('/api/login', name: 'api_login')]
+ #[Route('/api/login', name: 'api_login', methods: ['POST'])]
- public function index(): Response
+ public function index(#[CurrentUser] ?User $user): Response
{
+ if (null === $user) {
+ return $this->json([
+ 'message' => 'missing credentials',
+ ], Response::HTTP_UNAUTHORIZED);
+ }
+
+ $token = ...; // somehow create an API token for $user
+
return $this->json([
- 'message' => 'Welcome to your new controller!',
- 'path' => 'src/Controller/ApiLoginController.php',
+ 'user' => $user->getUserIdentifier(),
+ 'token' => $token,
]);
}
}
.. note::
The ``#[CurrentUser]`` can only be used in controller arguments to
retrieve the authenticated user. In services, you would use
:method:`Symfony\\Bundle\\SecurityBundle\\Security::getUser`.
That's it! To summarize the process:
#. A client (e.g. the front-end) makes a *POST request* with the
``Content-Type: application/json`` header to ``/api/login`` with
``username`` (even if your identifier is actually an email) and
``password`` keys:
.. code-block:: json
{
"username": "dunglas@example.com",
"password": "MyPassword"
}
#. The security system intercepts the request, checks the user's submitted
credentials and authenticates the user. If the credentials are incorrect,
an HTTP 401 Unauthorized JSON response is returned, otherwise your
controller is run;
#. Your controller creates the correct response:
.. code-block:: json
{
"user": "dunglas@example.com",
"token": "45be42..."
}
.. tip::
The JSON request format can be configured under the ``json_login`` key.
See :ref:`reference-security-firewall-json-login` for more details.
.. _security-http_basic:
HTTP Basic
~~~~~~~~~~
`HTTP Basic authentication`_ is a standardized HTTP authentication
framework. It asks credentials (username and password) using a dialog in
the browser and the HTTP basic authenticator of Symfony will verify these
credentials.
Add the ``http_basic`` key to your firewall to enable HTTP Basic
authentication:
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
# ...
firewalls:
main:
# ...
http_basic:
realm: Secured Area
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<firewall name="main">
<http-basic realm="Secured Area"/>
</firewall>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
$mainFirewall = $security->firewall('main');
$mainFirewall->httpBasic()
->realm('Secured Area')
;
};
That's it! Whenever an unauthenticated user tries to visit a protected
page, Symfony will inform the browser that it needs to start HTTP basic
authentication (using the ``WWW-Authenticate`` response header). Then, the
authenticator verifies the credentials and authenticates the user.
.. note::
You cannot use :ref:`log out <security-logging-out>` with the HTTP
basic authenticator. Even if you log out from Symfony, your browser
"remembers" your credentials and will send them on every request.
Login Link
~~~~~~~~~~
Login links are a passwordless authentication mechanism. The user will
receive a short-lived link (e.g. via email) which will authenticate them to the
website.
You can learn all about this authenticator in :doc:`/security/login_link`.
Access Tokens
~~~~~~~~~~~~~
Access Tokens are often used in API contexts.
The user receives a token from an authorization server
which authenticates them.
You can learn all about this authenticator in :doc:`/security/access_token`.
X.509 Client Certificates
~~~~~~~~~~~~~~~~~~~~~~~~~
When using client certificates, your web server does all the authentication
itself. The X.509 authenticator provided by Symfony extracts the email from
the "distinguished name" (DN) of the client certificate. Then, it uses this
email as user identifier in the user provider.
First, configure your web server to enable client certificate verification
and to expose the certificate's DN to the Symfony application:
.. configuration-block::
.. code-block:: nginx
server {
# ...
ssl_client_certificate /path/to/my-custom-CA.pem;
# enable client certificate verification
ssl_verify_client optional;
ssl_verify_depth 1;
location / {
# pass the DN as "SSL_CLIENT_S_DN" to the application
fastcgi_param SSL_CLIENT_S_DN $ssl_client_s_dn;
# ...
}
}
.. code-block:: apache
# ...
SSLCACertificateFile "/path/to/my-custom-CA.pem"
SSLVerifyClient optional
SSLVerifyDepth 1
# pass the DN to the application
SSLOptions +StdEnvVars
.. code-block:: caddy
tls {
client_auth {
mode verify_if_given # check the Caddy documentation for more information
trusted_ca_cert_file /path/to/my-custom-CA.pem
}
}
route {
# Other configuration options go here
php_fastcgi unix//var/run/php/php-fpm.sock {
env SSL_CLIENT_S_DN {tls_client_subject}
# Environment variables for other certificate fields that you might need.
# They are not used by Symfony, but you can use them in your application.
# See all placeholders: https://caddyserver.com/docs/caddyfile/concepts#placeholders
env SSL_CLIENT_S_FINGERPRINT {tls_client_fingerprint}
env SSL_CLIENT_S_CERTIFICATE {tls_client_certificate_der_base64}
env SSL_CLIENT_S_ISSUER {tls_client_issuer}
env SSL_CLIENT_S_SERIAL {tls_client_serial}
env SSL_CLIENT_S_VERSION {tls_version}
}
}
Then, enable the X.509 authenticator using ``x509`` on your firewall:
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
# ...
firewalls:
main:
# ...
x509:
provider: your_user_provider
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<firewall name="main">
<!-- ... -->
<x509 provider="your_user_provider"/>
</firewall>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
$mainFirewall = $security->firewall('main');
$mainFirewall->x509()
->provider('your_user_provider')
;
};
By default, Symfony extracts the email address from the DN in two different
ways:
#. First, it tries the ``SSL_CLIENT_S_DN_Email`` server parameter, which is
exposed by Apache;
#. If it is not set (e.g. when using Nginx), it uses ``SSL_CLIENT_S_DN`` and
matches the value following ``emailAddress``.
You can customize the name of some parameters under the ``x509`` key.
See :ref:`the x509 configuration reference <reference-security-firewall-x509>`
for more details.
Remote Users
~~~~~~~~~~~~
Besides client certificate authentication, there are more web server
modules that pre-authenticate a user (e.g. kerberos). The remote user
authenticator provides a basic integration for these services.
These modules often expose the authenticated user in the ``REMOTE_USER``
environment variable. The remote user authenticator uses this value as the
user identifier to load the corresponding user.
Enable remote user authentication using the ``remote_user`` key:
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
firewalls:
main:
# ...
remote_user:
provider: your_user_provider
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<firewall name="main">
<remote-user provider="your_user_provider"/>
</firewall>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
$mainFirewall = $security->firewall('main');
$mainFirewall->remoteUser()
->provider('your_user_provider')
;
};
.. tip::
You can customize the name of this server variable under the
``remote_user`` key. See
:ref:`the configuration reference <reference-security-firewall-remote-user>`
for more details.
.. _security-login-throttling:
Limiting Login Attempts
~~~~~~~~~~~~~~~~~~~~~~~
Symfony provides basic protection against `brute force login attacks`_ thanks to
the :doc:`Rate Limiter component </rate_limiter>`. If you haven't used this
component in your application yet, install it before using this feature:
.. code-block:: terminal
$ composer require symfony/rate-limiter
Then, enable this feature using the ``login_throttling`` setting:
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
firewalls:
# ...
main:
# ...
# by default, the feature allows 5 login attempts per minute
login_throttling: null
# configure the maximum login attempts
login_throttling:
max_attempts: 3 # per minute ...
# interval: '15 minutes' # ... or in a custom period
# use a custom rate limiter via its service ID
login_throttling:
limiter: app.my_login_rate_limiter
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<!-- you must use the authenticator manager -->
<config enable-authenticator-manager="true">
<!-- ... -->
<firewall name="main">
<!-- by default, the feature allows 5 login attempts per minute
max-attempts: (optional) You can configure the maximum attempts ...
interval: (optional) ... and the period of time. -->
<login-throttling max-attempts="3" interval="15 minutes"/>
<!-- use a custom rate limiter via its service ID -->
<login-throttling limiter="app.my_login_rate_limiter"/>
</firewall>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
$security->enableAuthenticatorManager(true);
$mainFirewall = $security->firewall('main');
// by default, the feature allows 5 login attempts per minute
$mainFirewall->loginThrottling()
// ->maxAttempts(3) // Optional: You can configure the maximum attempts ...
// ->interval('15 minutes') // ... and the period of time.
;
};
.. note::
The value of the ``interval`` option must be a number followed by any of the
units accepted by the `PHP date relative formats`_ (e.g. ``3 seconds``,
``10 hours``, ``1 day``, etc.)
Internally, Symfony uses the :doc:`Rate Limiter component </rate_limiter>`
which by default uses Symfony's cache to store the previous login attempts.
However, you can implement a :ref:`custom storage <rate-limiter-storage>`.
Login attempts are limited on ``max_attempts`` (default: 5)
failed requests for ``IP address + username`` and ``5 * max_attempts``
failed requests for ``IP address``. The second limit protects against an
attacker using multiple usernames from bypassing the first limit, without
disrupting normal users on big networks (such as offices).
.. tip::
Limiting the failed login attempts is only one basic protection against
brute force attacks. The `OWASP Brute Force Attacks`_ guidelines mention
several other protections that you should consider depending on the
level of protection required.
If you need a more complex limiting algorithm, create a class that implements
:class:`Symfony\\Component\\HttpFoundation\\RateLimiter\\RequestRateLimiterInterface`
(or use
:class:`Symfony\\Component\\Security\\Http\\RateLimiter\\DefaultLoginRateLimiter`)
and set the ``limiter`` option to its service ID:
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
framework:
rate_limiter:
# define 2 rate limiters (one for username+IP, the other for IP)
username_ip_login:
policy: token_bucket
limit: 5
rate: { interval: '5 minutes' }
ip_login:
policy: sliding_window
limit: 50
interval: '15 minutes'
services:
# our custom login rate limiter
app.login_rate_limiter:
class: Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter
arguments:
# globalFactory is the limiter for IP
$globalFactory: '@limiter.ip_login'
# localFactory is the limiter for username+IP
$localFactory: '@limiter.username_ip_login'
$secret: '%kernel.secret%'
security:
firewalls:
main:
# use a custom rate limiter via its service ID
login_throttling:
limiter: app.login_rate_limiter
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony
https://symfony.com/schema/dic/symfony/symfony-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<framework:config>
<framework:rate-limiter>
<!-- define 2 rate limiters (one for username+IP, the other for IP) -->
<framework:limiter name="username_ip_login"
policy="token_bucket"
limit="5"
>
<framework:rate interval="5 minutes"/>
</framework:limiter>
<framework:limiter name="ip_login"
policy="sliding_window"
limit="50"
interval="15 minutes"
/>
</framework:rate-limiter>
</framework:config>
<srv:services>
<!-- our custom login rate limiter -->
<srv:service id="app.login_rate_limiter"
class="Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter"
>
<!-- 1st argument is the limiter for IP -->
<srv:argument type="service" id="limiter.ip_login"/>
<!-- 2nd argument is the limiter for username+IP -->
<srv:argument type="service" id="limiter.username_ip_login"/>
<!-- 3rd argument is the app secret -->
<srv:argument type="string">%kernel.secret%</srv:argument>
</srv:service>
</srv:services>
<config>
<firewall name="main">
<!-- use a custom rate limiter via its service ID -->
<login-throttling limiter="app.login_rate_limiter"/>
</firewall>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter;
use Symfony\Config\FrameworkConfig;
use Symfony\Config\SecurityConfig;
return static function (ContainerBuilder $container, FrameworkConfig $framework, SecurityConfig $security): void {
$framework->rateLimiter()
->limiter('username_ip_login')
->policy('token_bucket')
->limit(5)
->rate()
->interval('5 minutes')
;
$framework->rateLimiter()
->limiter('ip_login')
->policy('sliding_window')
->limit(50)
->interval('15 minutes')
;
$container->register('app.login_rate_limiter', DefaultLoginRateLimiter::class)
->setArguments([
// 1st argument is the limiter for IP
new Reference('limiter.ip_login'),
// 2nd argument is the limiter for username+IP
new Reference('limiter.username_ip_login'),
// 3rd argument is the app secret
param('kernel.secret'),
]);
$security->firewall('main')
->loginThrottling()
->limiter('app.login_rate_limiter')
;
};
Customize Successful and Failed Authentication Behavior
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to customize how the successful or failed authentication process is
handled, you don't have to overwrite the respective listeners globally. Instead,
you can set custom success failure handlers by implementing the
:class:`Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationSuccessHandlerInterface`
or the
:class:`Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationFailureHandlerInterface`.
Read :ref:`how to customize your success handler <login-link_customize-success-handler>`
for more information about this.
Login Programmatically
----------------------
You can log in a user programmatically using the ``login()`` method of the
:class:`Symfony\\Bundle\\SecurityBundle\\Security` helper::
// src/Controller/SecurityController.php
namespace App\Controller;
use App\Security\Authenticator\ExampleAuthenticator;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
class SecurityController
{
public function someAction(Security $security): Response
{
// get the user to be authenticated
$user = ...;
// log the user in on the current firewall
$security->login($user);
// if the firewall has more than one authenticator, you must pass it explicitly
// by using the name of built-in authenticators...
$security->login($user, 'form_login');
// ...or the service id of custom authenticators
$security->login($user, ExampleAuthenticator::class);
// you can also log in on a different firewall...
$security->login($user, 'form_login', 'other_firewall');
// ... add badges...
$security->login($user, 'form_login', 'other_firewall', [(new RememberMeBadge())->enable()]);
// ... and also add passport attributes
$security->login($user, 'form_login', 'other_firewall', [(new RememberMeBadge())->enable()], ['referer' => 'https://oauth.example.com']);
// use the redirection logic applied to regular login
$redirectResponse = $security->login($user);
return $redirectResponse;
// or use a custom redirection logic (e.g. redirect users to their account page)
// return new RedirectResponse('...');
}
}
.. versionadded:: 7.2
The support for passport attributes in the
:method:`Symfony\\Bundle\\SecurityBundle\\Security::login` method was
introduced in Symfony 7.2.
.. _security-logging-out:
Logging Out
-----------
To enable logging out, activate the ``logout`` config parameter under your firewall:
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
# ...
firewalls:
main:
# ...
logout:
path: /logout
# where to redirect after logout
# target: app_any_route
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<firewall name="main">
<logout path="/logout"/>
<!-- use "target" to configure where to redirect after logout
<logout path="/logout" target="app_any_route"/>
-->
</firewall>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
// ...
$mainFirewall = $security->firewall('main');
// ...
$mainFirewall->logout()
->path('/logout')
// where to redirect after logout
// ->target('app_any_route')
;
};
Symfony will then un-authenticate users navigating to the configured ``path``,
and redirect them to the configured ``target``.
.. tip::
If you need to reference the logout path, you can use the ``_logout_<firewallname>``
route name (e.g. ``_logout_main``).
If your project does not use :ref:`Symfony Flex <symfony-flex>`, make sure
you have imported the logout route loader in your routes:
.. configuration-block::
.. code-block:: yaml
# config/routes/security.yaml
_symfony_logout:
resource: security.route_loader.logout
type: service
.. code-block:: xml
<!-- config/routes/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
https://symfony.com/schema/routing/routing-1.0.xsd">
<import resource="security.route_loader.logout" type="service"/>
</routes>
.. code-block:: php
// config/routes/security.php
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
return static function (RoutingConfigurator $routes): void {
$routes->import('security.route_loader.logout', 'service');
};
Logout programmatically
~~~~~~~~~~~~~~~~~~~~~~~
You can logout user programmatically using the ``logout()`` method of the
:class:`Symfony\\Bundle\\SecurityBundle\\Security` helper::
// src/Controller/SecurityController.php
namespace App\Controller;
use Symfony\Bundle\SecurityBundle\Security;
class SecurityController
{
public function someAction(Security $security): Response
{
// logout the user in on the current firewall
$response = $security->logout();
// you can also disable the csrf logout
$response = $security->logout(false);
// ... return $response (if set) or e.g. redirect to the homepage
}
}
The user will be logged out from the firewall of the request. If the request is
not behind a firewall a ``\LogicException`` will be thrown.
Customizing Logout
~~~~~~~~~~~~~~~~~~
In some cases you need to run extra logic upon logout (e.g. invalidate
some tokens) or want to customize what happens after a logout. During
logout, a :class:`Symfony\\Component\\Security\\Http\\Event\\LogoutEvent`
is dispatched. Register an :doc:`event listener or subscriber </event_dispatcher>`
to execute custom logic::
// src/EventListener/LogoutSubscriber.php
namespace App\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
class LogoutSubscriber implements EventSubscriberInterface
{
public function __construct(
private UrlGeneratorInterface $urlGenerator
) {
}
public static function getSubscribedEvents(): array
{
return [LogoutEvent::class => 'onLogout'];
}
public function onLogout(LogoutEvent $event): void
{
// get the security token of the session that is about to be logged out
$token = $event->getToken();
// get the current request
$request = $event->getRequest();
// get the current response, if it is already set by another listener
$response = $event->getResponse();
// configure a custom logout response to the homepage
$response = new RedirectResponse(
$this->urlGenerator->generate('homepage'),
RedirectResponse::HTTP_SEE_OTHER
);
$event->setResponse($response);
}
}
Customizing Logout Path
~~~~~~~~~~~~~~~~~~~~~~~
Another option is to configure ``path`` as a route name. This can be useful
if you want logout URIs to be dynamic (e.g. translated according to the
current locale). In that case, you have to create this route yourself:
.. configuration-block::
.. code-block:: yaml
# config/routes.yaml
app_logout:
path:
en: /logout
fr: /deconnexion
methods: GET
.. code-block:: xml
<!-- config/routes.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
https://symfony.com/schema/routing/routing-1.0.xsd">
<route id="app_logout" path="/logout" methods="GET">
<path locale="en">/logout</path>
<path locale="fr">/deconnexion</path>
</route>
</routes>
.. code-block:: php
// config/routes.php
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
return function (RoutingConfigurator $routes): void {
$routes->add('app_logout', [
'en' => '/logout',
'fr' => '/deconnexion',
])
->methods(['GET'])
;
};
Then, pass the route name to the ``path`` option:
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
# ...
firewalls:
main:
# ...
logout:
path: app_logout
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<firewall name="main">
<logout path="app_logout"/>
</firewall>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
// ...
$mainFirewall = $security->firewall('main');
// ...
$mainFirewall->logout()
->path('app_logout')
;
};
.. _retrieving-the-user-object:
Fetching the User Object
------------------------
After authentication, the ``User`` object of the current user can be
accessed via the ``getUser()`` shortcut in the
:ref:`base controller <the-base-controller-class-services>`::
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
class ProfileController extends AbstractController
{
public function index(): Response
{
// usually you'll want to make sure the user is authenticated first,
// see "Authorization" below
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
// returns your User object, or null if the user is not authenticated
// use inline documentation to tell your editor your exact User class
/** @var \App\Entity\User $user */
$user = $this->getUser();
// Call whatever methods you've added to your User class
// For example, if you added a getFirstName() method, you can use that.
return new Response('Well hi there '.$user->getFirstName());
}
}
Fetching the User from a Service
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you need to get the logged in user from a service, use the
:class:`Symfony\\Bundle\\SecurityBundle\\Security` service::
// src/Service/ExampleService.php
// ...
use Symfony\Bundle\SecurityBundle\Security;
class ExampleService
{
// Avoid calling getUser() in the constructor: auth may not
// be complete yet. Instead, store the entire Security object.
public function __construct(
private Security $security,
){
}
public function someMethod(): void
{
// returns User object or null if not authenticated
$user = $this->security->getUser();
// ...
}
}
Fetch the User in a Template
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In a Twig Template the user object is available via the ``app.user`` variable
thanks to the :ref:`Twig global app variable <twig-app-variable>`:
.. code-block:: html+twig
{% if is_granted('IS_AUTHENTICATED_FULLY') %}
<p>Email: {{ app.user.email }}</p>
{% endif %}
.. _denying-access-roles-and-other-authorization:
.. _security-access-control:
Access Control (Authorization)
------------------------------
Users can now log in to your app using your login form. Great! Now, you need to learn
how to deny access and work with the User object. This is called **authorization**,
and its job is to decide if a user can access some resource (a URL, a model object,
a method call, ...).
The process of authorization has two different sides:
#. The user receives a specific role when logging in (e.g. ``ROLE_ADMIN``).
#. You add code so that a resource (e.g. URL, controller) requires a specific
"attribute" (e.g. a role like ``ROLE_ADMIN``) in order to be accessed.
Roles
~~~~~
When a user logs in, Symfony calls the ``getRoles()`` method on your ``User``
object to determine which roles this user has. In the ``User`` class that
was generated earlier, the roles are an array that's stored in the
database and every user is *always* given at least one role: ``ROLE_USER``::
// src/Entity/User.php
// ...
class User
{
#[ORM\Column(type: 'json')]
private array $roles = [];
// ...
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
}
This is a nice default, but you can do *whatever* you want to determine which roles
a user should have. The only rule is that every role **must start with** the
``ROLE_`` prefix - otherwise, things won't work as expected. Other than that,
a role is just a string and you can invent whatever you need (e.g. ``ROLE_PRODUCT_ADMIN``).
You'll use these roles next to grant access to specific sections of your site.
.. _security-role-hierarchy:
Hierarchical Roles
..................
Instead of giving many roles to each user, you can define role inheritance
rules by creating a role hierarchy:
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
# ...
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<role id="ROLE_ADMIN">ROLE_USER</role>
<role id="ROLE_SUPER_ADMIN">ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH</role>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
// ...
$security->roleHierarchy('ROLE_ADMIN', ['ROLE_USER']);
$security->roleHierarchy('ROLE_SUPER_ADMIN', ['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH']);
};
Users with the ``ROLE_ADMIN`` role will also have the ``ROLE_USER`` role.
Users with ``ROLE_SUPER_ADMIN``, will automatically have ``ROLE_ADMIN``,
``ROLE_ALLOWED_TO_SWITCH`` and ``ROLE_USER`` (inherited from
``ROLE_ADMIN``).
.. warning::
For role hierarchy to work, do not use ``$user->getRoles()`` manually.
For example, in a controller extending from the :ref:`base controller <the-base-controller-class-services>`::
// BAD - $user->getRoles() will not know about the role hierarchy
$hasAccess = in_array('ROLE_ADMIN', $user->getRoles());
// GOOD - use of the normal security methods
$hasAccess = $this->isGranted('ROLE_ADMIN');
$this->denyAccessUnlessGranted('ROLE_ADMIN');
.. note::
The ``role_hierarchy`` values are static - you can't, for example, store the
role hierarchy in a database. If you need that, create a custom
:doc:`security voter </security/voters>` that looks for the user roles
in the database.
.. _security-role-authorization:
Add Code to Deny Access
~~~~~~~~~~~~~~~~~~~~~~~
There are **two** ways to deny access to something:
#. :ref:`access_control in security.yaml <security-authorization-access-control>`
allows you to protect URL patterns (e.g. ``/admin/*``). Simpler, but less flexible;
#. :ref:`in your controller (or other code) <security-securing-controller>`.
.. _security-authorization-access-control:
Securing URL patterns (access_control)
......................................
The most basic way to secure part of your app is to secure an entire URL pattern
in ``security.yaml``. For example, to require ``ROLE_ADMIN`` for all URLs that
start with ``/admin``, you can:
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
# ...
firewalls:
# ...
main:
# ...
access_control:
# require ROLE_ADMIN for /admin*
- { path: '^/admin', roles: ROLE_ADMIN }
# or require ROLE_ADMIN or IS_AUTHENTICATED_FULLY for /admin*
- { path: '^/admin', roles: [IS_AUTHENTICATED_FULLY, ROLE_ADMIN] }
# the 'path' value can be any valid regular expression
# (this one will match URLs like /api/post/7298 and /api/comment/528491)
- { path: ^/api/(post|comment)/\d+$, roles: ROLE_USER }
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<firewall name="main">
<!-- ... -->
</firewall>
<!-- require ROLE_ADMIN for /admin* -->
<rule path="^/admin" role="ROLE_ADMIN"/>
<!-- require ROLE_ADMIN or IS_AUTHENTICATED_FULLY for /admin* -->
<rule path="^/admin">
<role>ROLE_ADMIN</role>
<role>IS_AUTHENTICATED_FULLY</role>
</rule>
<!-- the 'path' value can be any valid regular expression
(this one will match URLs like /api/post/7298 and /api/comment/528491) -->
<rule path="^/api/(post|comment)/\d+$" role="ROLE_USER"/>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
$security->enableAuthenticatorManager(true);
// ...
$security->firewall('main')
// ...
;
// require ROLE_ADMIN for /admin*
$security->accessControl()
->path('^/admin')
->roles(['ROLE_ADMIN']);
// require ROLE_ADMIN or IS_AUTHENTICATED_FULLY for /admin*
$security->accessControl()
->path('^/admin')
->roles(['ROLE_ADMIN', 'IS_AUTHENTICATED_FULLY']);
// the 'path' value can be any valid regular expression
// (this one will match URLs like /api/post/7298 and /api/comment/528491)
$security->accessControl()
->path('^/api/(post|comment)/\d+$')
->roles(['ROLE_USER']);
};
You can define as many URL patterns as you need - each is a regular expression.
**BUT**, only **one** will be matched per request: Symfony starts at the top of
the list and stops when it finds the first match:
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
# ...
access_control:
# matches /admin/users/*
- { path: '^/admin/users', roles: ROLE_SUPER_ADMIN }
# matches /admin/* except for anything matching the above rule
- { path: '^/admin', roles: ROLE_ADMIN }
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<!-- ... -->
<rule path="^/admin/users" role="ROLE_SUPER_ADMIN"/>
<rule path="^/admin" role="ROLE_ADMIN"/>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
// ...
$security->accessControl()
->path('^/admin/users')
->roles(['ROLE_SUPER_ADMIN']);
$security->accessControl()
->path('^/admin')
->roles(['ROLE_ADMIN']);
};
Prepending the path with ``^`` means that only URLs *beginning* with the
pattern are matched. For example, a path of ``/admin`` (without the ``^``)
would match ``/admin/foo`` but would also match URLs like ``/foo/admin``.
Each ``access_control`` can also match on IP address, hostname and HTTP methods.
It can also be used to redirect a user to the ``https`` version of a URL pattern.
For more complex needs, you can also use a service implementing ``RequestMatcherInterface``.
See :doc:`/security/access_control`.
.. _security-securing-controller:
Securing Controllers and other Code
...................................
You can deny access from inside a controller::
// src/Controller/AdminController.php
// ...
public function adminDashboard(): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
// or add an optional message - seen by developers
$this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'User tried to access a page without having ROLE_ADMIN');
}
That's it! If access is not granted, a special
:class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException`
is thrown and no more code in your controller is called. Then, one of two things
will happen:
1) If the user isn't logged in yet, they will be asked to log in (e.g. redirected
to the login page).
2) If the user *is* logged in, but does *not* have the ``ROLE_ADMIN`` role, they'll
be shown the 403 access denied page (which you can
:ref:`customize <controller-error-pages-by-status-code>`).
.. _security-securing-controller-annotations:
.. _security-securing-controller-attributes:
Another way to secure one or more controller actions is to use the ``#[IsGranted]`` attribute.
In the following example, all controller actions will require the
``ROLE_ADMIN`` permission, except for ``adminDashboard()``, which will require
the ``ROLE_SUPER_ADMIN`` permission:
.. code-block:: php-attributes
// src/Controller/AdminController.php
// ...
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_ADMIN')]
class AdminController extends AbstractController
{
// Optionally, you can set a custom message that will be displayed to the user
#[IsGranted('ROLE_SUPER_ADMIN', message: 'You are not allowed to access the admin dashboard.')]
public function adminDashboard(): Response
{
// ...
}
}
If you want to use a custom status code instead of the default one (which
is 403), this can be done by setting with the ``statusCode`` argument::
// src/Controller/AdminController.php
// ...
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_ADMIN', statusCode: 423)]
class AdminController extends AbstractController
{
// ...
}
You can also set the internal exception code of the
:class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException`
that is thrown with the ``exceptionCode`` argument::
// src/Controller/AdminController.php
// ...
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_ADMIN', statusCode: 403, exceptionCode: 10010)]
class AdminController extends AbstractController
{
// ...
}
.. _security-template:
Access Control in Templates
...........................
If you want to check if the current user has a certain role, you can use
the built-in ``is_granted()`` helper function in any Twig template:
.. code-block:: html+twig
{% if is_granted('ROLE_ADMIN') %}
<a href="...">Delete</a>
{% endif %}
.. _security-isgranted:
Similarly, if you want to check if a specific user has a certain role, you can use
the built-in ``is_granted_for_user()`` helper function:
.. code-block:: html+twig
{% if is_granted_for_user(user, 'ROLE_ADMIN') %}
<a href="...">Delete</a>
{% endif %}
.. _security-isgrantedforuser:
Securing other Services
.......................
You can check access *anywhere* in your code by injecting the ``Security``
service. For example, suppose you have a ``SalesReportManager`` service and you
want to include extra details only for users that have a ``ROLE_SALES_ADMIN`` role:
.. code-block:: diff
// src/SalesReport/SalesReportManager.php
// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
+ use Symfony\Bundle\SecurityBundle\Security;
class SalesReportManager
{
+ public function __construct(
+ private Security $security,
+ ) {
+ }
public function generateReport(): void
{
$salesData = [];
+ if ($this->security->isGranted('ROLE_SALES_ADMIN')) {
+ $salesData['top_secret_numbers'] = rand();
+ }
// ...
}
// ...
}
.. tip::
The ``isGranted()`` method checks authorization for the currently logged-in user.
If you need to check authorization for a different user or when the user session
is unavailable (e.g., in a CLI context such as a message queue or cron job), you
can use the ``isGrantedForUser()`` method to explicitly set the target user.
.. versionadded:: 7.3
The :method:`Symfony\\Bundle\\SecurityBundle\\Security::isGrantedForUser`
method was introduced in Symfony 7.3.
If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>`,
Symfony will automatically pass the ``security.helper`` to your service
thanks to autowiring and the ``Security`` type-hint.
You can also use a lower-level
:class:`Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationCheckerInterface`
service. It does the same thing as ``Security``, but allows you to type-hint a
more-specific interface.
Allowing Unsecured Access (i.e. Anonymous Users)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When a visitor isn't yet logged in to your website, they are treated as
"unauthenticated" and don't have any roles. This will block them from
visiting your pages if you defined an ``access_control`` rule.
In the ``access_control`` configuration, you can use the ``PUBLIC_ACCESS``
security attribute to exclude some routes for unauthenticated access (e.g.
the login page):
.. configuration-block::
.. code-block:: yaml
# config/packages/security.yaml
security:
# ...
access_control:
# allow unauthenticated users to access the login form
- { path: ^/admin/login, roles: PUBLIC_ACCESS }
# but require authentication for all other admin routes
- { path: ^/admin, roles: ROLE_ADMIN }
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config enable-authenticator-manager="true">
<!-- ... -->
<access-control>
<!-- allow unauthenticated users to access the login form -->
<rule path="^/admin/login" role="PUBLIC_ACCESS"/>
<!-- but require authentication for all other admin routes -->
<rule path="^/admin" role="ROLE_ADMIN"/>
</access-control>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
use Symfony\Config\SecurityConfig;
return static function (SecurityConfig $security): void {
$security->enableAuthenticatorManager(true);
// ....
// allow unauthenticated users to access the login form
$security->accessControl()
->path('^/admin/login')
->roles([AuthenticatedVoter::PUBLIC_ACCESS])
;
// but require authentication for all other admin routes
$security->accessControl()
->path('^/admin')
->roles(['ROLE_ADMIN'])
;
};
Granting Anonymous Users Access in a Custom Voter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you're using a :doc:`custom voter </security/voters>`, you can allow
anonymous users access by checking if there is no user set on the token::
// src/Security/PostVoter.php
namespace App\Security;
// ...
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\User\UserInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class PostVoter extends Voter
{
// ...
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool
{
// ...
if (!$token->getUser() instanceof UserInterface) {
// the user is not authenticated, e.g. only allow them to
// see public posts
return $subject->isPublic();
}
}
}
.. versionadded:: 7.3
The ``$vote`` argument of the ``voteOnAttribute()`` method was introduced
in Symfony 7.3.
Setting Individual User Permissions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Most applications require more specific access rules. For instance, a user
should be able to only edit their *own* comments on a blog. Voters allow you
to write *whatever* business logic you need to determine access. Using
these voters is similar to the role-based access checks implemented in the
previous chapters. Read :doc:`/security/voters` to learn how to implement
your own voter.
.. _checking-to-see-if-a-user-is-logged-in-is-authenticated-fully:
Checking to see if a User is Logged In
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you *only* want to check if a user is logged in (you don't care about roles),
you have the following two options.
Firstly, if you've given *every* user ``ROLE_USER``, you can check for that role.
Secondly, you can use the special "attribute" ``IS_AUTHENTICATED`` in place of a role::
// ...
public function adminDashboard(): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED');
// ...
}
You can use ``IS_AUTHENTICATED`` anywhere roles are used: like
``access_control`` or in Twig.
``IS_AUTHENTICATED`` isn't a role, but it kind of acts like one, and every
user that has logged in will have this. Actually, there are some special attributes
like this:
* ``IS_AUTHENTICATED_FULLY``: This is similar to ``IS_AUTHENTICATED_REMEMBERED``,
but stronger. Users who are logged in only because of a "remember me cookie"
will have ``IS_AUTHENTICATED_REMEMBERED`` but will not have ``IS_AUTHENTICATED_FULLY``.
* ``IS_REMEMBERED``: *Only* users authenticated using the
:doc:`remember me functionality </security/remember_me>`, (i.e. a
remember-me cookie).
* ``IS_IMPERSONATOR``: When the current user is
:doc:`impersonating </security/impersonating_user>` another user in this
session, this attribute will match.
.. _user_session_refresh:
Understanding how Users are Refreshed from the Session
------------------------------------------------------
At the end of every request (unless your firewall is ``stateless``), your
``User`` object is serialized to the session. At the beginning of the next
request, it's deserialized and then passed to your user provider to "refresh" it
(e.g. Doctrine queries for a fresh user).
Then, the two User objects (the original from the session and the refreshed User
object) are "compared" to see if they are "equal". By default, the core
``AbstractToken`` class compares the return values of the ``getPassword()``,
``getSalt()`` and ``getUserIdentifier()`` methods. If any of these are different,
your user will be logged out. This is a security measure to make sure that malicious
users can be de-authenticated if core user data changes.
Storing the (plain or hashed) password in the session can be a security risk.
To mitigate this, implement the ``__serialize()`` magic method in your user class
to exclude or transform the password before storing the serialized user object
in the session.
Two strategies are supported:
#. Remove the password completely. After unserialization, ``getPassword()`` returns
``null`` and Symfony refreshes the user without checking the password. Use this
only if you store plaintext passwords (not recommended).
#. Hash the password using the ``crc32c`` algorithm. Symfony will hash the password
of the refreshed user and compare it to the session value. This approach avoids
storing the real hash and lets you invalidate sessions on password change.
Example (assuming the password is stored in a private property called ``password``)::
public function __serialize(): array
{
$data = (array) $this;
$data["\0".self::class."\0password"] = hash('crc32c', $this->password);
return $data;
}
.. versionadded:: 7.3
Support for hashing passwords with ``crc32c`` in session serialization was
introduced in Symfony 7.3.
If you're having problems authenticating, it could be that you *are* authenticating
successfully, but you immediately lose authentication after the first redirect.
In that case, review the serialization logic (e.g. the ``__serialize()`` or
``serialize()`` methods) on your user class (if you have any) to make sure
that all the fields necessary are serialized and also exclude all the
fields not necessary to be serialized (e.g. Doctrine relations).
Comparing Users Manually with EquatableInterface
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Or, if you need more control over the "compare users" process, make your User class
implement :class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface`.
Then, your ``isEqualTo()`` method will be called when comparing users instead
of the core logic.
.. _security-security-events:
Security Events
---------------
During the authentication process, multiple events are dispatched that allow you
to hook into the process or customize the response sent back to the user. You
can do this by creating an :doc:`event listener or subscriber </event_dispatcher>`
for these events.
.. tip::
Every Security firewall has its own event dispatcher
(``security.event_dispatcher.FIREWALLNAME``). Events are dispatched on
both the global and the firewall-specific dispatcher. You can register
on the firewall dispatcher if you want your listener to only be
called for a specific firewall. For instance, if you have an ``api``
and ``main`` firewall, use this configuration to register only on the
logout event in the ``main`` firewall:
.. configuration-block::
.. code-block:: yaml
# config/services.yaml
services:
# ...
App\EventListener\LogoutSubscriber:
tags:
- name: kernel.event_subscriber
dispatcher: security.event_dispatcher.main
.. code-block:: xml
<!-- config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<!-- ... -->
<service id="App\EventListener\LogoutSubscriber">
<tag name="kernel.event_subscriber"
dispatcher="security.event_dispatcher.main"
/>
</service>
</services>
</container>
.. code-block:: php
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\EventListener\LogoutSubscriber;
return function(ContainerConfigurator $container): void {
$services = $container->services();
$services->set(LogoutSubscriber::class)
->tag('kernel.event_subscriber', [
'dispatcher' => 'security.event_dispatcher.main',
]);
};
Authentication Events
~~~~~~~~~~~~~~~~~~~~~
.. raw:: html
<object data="_images/security/security_events.svg" type="image/svg+xml"
alt="A flow diagram showing the authentication events that are described in this section in a request-response cycle."
></object>
:class:`Symfony\\Component\\Security\\Http\\Event\\CheckPassportEvent`
Dispatched after the authenticator created the :ref:`security passport <security-passport>`.
Listeners of this event do the actual authentication checks (like
checking the passport, validating the CSRF token, etc.)
:class:`Symfony\\Component\\Security\\Http\\Event\\AuthenticationTokenCreatedEvent`
Dispatched after the passport was validated and the authenticator
created the security token (and user). This can be used in advanced use-cases
where you need to modify the created token (e.g. for multi factor
authentication).
:class:`Symfony\\Component\\Security\\Core\\Event\\AuthenticationSuccessEvent`
Dispatched when authentication is nearing success. This is the last
event that can make an authentication fail by throwing an
``AuthenticationException``.
:class:`Symfony\\Component\\Security\\Http\\Event\\LoginSuccessEvent`
Dispatched after authentication was fully successful. Listeners to this
event can modify the response sent back to the user.
:class:`Symfony\\Component\\Security\\Http\\Event\\LoginFailureEvent`
Dispatched after an ``AuthenticationException`` was thrown during
authentication. Listeners to this event can modify the error response
sent back to the user.
Other Events
~~~~~~~~~~~~
:class:`Symfony\\Component\\Security\\Http\\Event\\InteractiveLoginEvent`
Dispatched after authentication was fully successful only when the authenticator
implements :class:`Symfony\\Component\\Security\\Http\\Authenticator\\InteractiveAuthenticatorInterface`,
which indicates login requires explicit user action (e.g. a login form).
Listeners to this event can modify the response sent back to the user.
:class:`Symfony\\Component\\Security\\Http\\Event\\LogoutEvent`
Dispatched just before a user logs out of your application. See
:ref:`security-logging-out`.
:class:`Symfony\\Component\\Security\\Http\\Event\\TokenDeauthenticatedEvent`
Dispatched when a user is deauthenticated, for instance because the
password was changed. See :ref:`user_session_refresh`.
:class:`Symfony\\Component\\Security\\Http\\Event\\SwitchUserEvent`
Dispatched after impersonation is completed. See
:doc:`/security/impersonating_user`.
Frequently Asked Questions
--------------------------
**Can I have Multiple Firewalls?**
Yes! However, each firewall is like a separate security system: being authenticated
in one firewall doesn't make you authenticated in another one. Each firewall can have
multiple ways of allowing authentication (e.g. form login, and API key authentication).
If you want to share authentication between firewalls, you have to explicitly
specify the same :ref:`reference-security-firewall-context` for different firewalls.
**Security doesn't seem to work on my Error Pages**
As routing is done *before* security, 404 error pages are not covered by
any firewall. This means you can't check for security or even access the
user object on these pages. See :doc:`/controller/error_pages`
for more details.
**My Authentication Doesn't Seem to Work: No Errors, but I'm Never Logged In**
Sometimes authentication may be successful, but after redirecting, you're
logged out immediately due to a problem loading the ``User`` from the session.
To see if this is an issue, check your log file (``var/log/dev.log``) for
the log message.
**Cannot refresh token because user has changed**
If you see this, there are two possible causes. First, there may be a problem
loading your User from the session. See :ref:`user_session_refresh`. Second,
if certain user information was changed in the database since the last page
refresh, Symfony will purposely log out the user for security reasons.
Learn More
----------
Authentication (Identifying/Logging in the User)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. toctree::
:maxdepth: 1
security/passwords
security/ldap
security/remember_me
security/impersonating_user
security/user_checkers
security/firewall_restriction
security/csrf
security/form_login
security/custom_authenticator
security/entry_point
Authorization (Denying Access)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. toctree::
:maxdepth: 1
security/voters
security/access_control
security/expressions
security/access_denied_handler
security/force_https
.. _`HWIOAuthBundle`: https://github.com/hwi/HWIOAuthBundle
.. _`OWASP Brute Force Attacks`: https://owasp.org/www-community/controls/Blocking_Brute_Force_Attacks
.. _`brute force login attacks`: https://owasp.org/www-community/controls/Blocking_Brute_Force_Attacks
.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html
.. _`SymfonyCastsVerifyEmailBundle`: https://github.com/symfonycasts/verify-email-bundle
.. _`HTTP Basic authentication`: https://en.wikipedia.org/wiki/Basic_access_authentication
.. _`Login CSRF attacks`: https://en.wikipedia.org/wiki/Cross-site_request_forgery#Forging_login_requests
.. _`PHP date relative formats`: https://www.php.net/manual/en/datetime.formats.php#datetime.formats.relative
.. _`Oauth2-client`: https://github.com/thephpleague/oauth2-client