mirror of
https://github.com/symfony/symfony-docs.git
synced 2026-03-23 16:22:10 +01:00
3026 lines
104 KiB
ReStructuredText
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
|