Using the new Authenticator-based Security

5.4 version
Symfony 5.4 is backed by Private Packagist.

Using the new Authenticator-based Security

New in version 5.1: Authenticator-based security was introduced in Symfony 5.1.

In Symfony 5.1, a new authentication system was introduced. This system changes the internals of Symfony Security, to make it more extensible and more understandable.

Enabling the System

The authenticator-based system can be enabled using the enable_authenticator_manager setting:

  • YAML
    1
    2
    3
    4
    # config/packages/security.yaml
    security:
        enable_authenticator_manager: true
        # ...
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <!-- 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">
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    // config/packages/security.php
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        $security->enableAuthenticatorManager(true);
        // ....
    };
    

The new system is backwards compatible with the current authentication system, with some exceptions that will be explained in this article:

Adding Support for Unsecured Access (i.e. Anonymous Users)

In Symfony, visitors that haven’t yet logged in to your website were called anonymous users. The new system no longer has anonymous authentication. Instead, these sessions are now treated as unauthenticated (i.e. there is no security token). When using isGranted(), the result will always be false (i.e. denied) as this session is handled as a user without any privileges.

In the access_control configuration, you can use the new PUBLIC_ACCESS security attribute to whitelist some routes for unauthenticated access (e.g. the login page):

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    # config/packages/security.yaml
    security:
        enable_authenticator_manager: true
    
        # ...
        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 }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <!-- 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>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // config/packages/security.php
    use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        $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

New in version 5.2: The NullToken class was introduced in Symfony 5.2.

If you’re using a custom voter, you can allow anonymous users access by checking for a special Symfony\Component\Security\Core\Authentication\Token\NullToken. This token is used in the voters to represent the unauthenticated access:

// src/Security/PostVoter.php
namespace App\Security;

// ...
use Symfony\Component\Security\Core\Authentication\Token\NullToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class PostVoter extends Voter
{
    // ...

    protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
    {
        // ...

        if ($token instanceof NullToken) {
            // the user is not authenticated, e.g. only allow them to
            // see public posts
            return $subject->isPublic();
        }
    }
}

Configuring the Authentication Entry Point

Sometimes, one firewall has multiple ways to authenticate (e.g. both a form login and an API token authentication). In these cases, it is now required to configure the authentication entry point. The entry point is used to generate a response when the user is not yet authenticated but tries to access a page that requires authentication. This can be used for instance to redirect the user to the login page.

You can configure this using the entry_point setting:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    # config/packages/security.yaml
    security:
        enable_authenticator_manager: true
    
        # ...
        firewalls:
            main:
                # allow authentication using a form or HTTP basic
                form_login: ~
                http_basic: ~
    
                # configure the form authentication as the entry point for unauthenticated users
                entry_point: form_login
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <!-- 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">
            <!-- ... -->
    
            <!-- entry-point: configure the form authentication as the entry
                              point for unauthenticated users -->
            <firewall name="main"
                entry-point="form_login"
            >
                <!-- allow authentication using a form or HTTP basic -->
                <form-login/>
                <http-basic/>
            </firewall>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    // config/packages/security.php
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        $security->enableAuthenticatorManager(true);
        // ....
    
    
        // allow authentication using a form or HTTP basic
        $mainFirewall = $security->firewall('main');
        $mainFirewall->formLogin();
        $mainFirewall->httpBasic();
    
        // configure the form authentication as the entry point for unauthenticated users
        $mainFirewall
            ->entryPoint('form_login');
    };
    

Note

You can also create your own authentication entry point by creating a class that implements Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface. You can then set entry_point to the service id (e.g. entry_point: App\Security\CustomEntryPoint)

Creating a Custom Authenticator

Security traditionally could be extended by writing custom authentication providers. The authenticator-based system dropped support for these providers and introduced a new authenticator interface as a base for custom authentication methods.

Tip

Guard authenticators are still supported in the authenticator-based system. It is however recommended to also update these when you’re refactoring your application to the new system. The new authenticator interface has many similarities with the guard authenticator interface, making the rewrite easier.

Authenticators should implement the Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface. You can also extend Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator, which has a default implementation for the createAuthenticatedToken() method that fits most use-cases:

// src/Security/ApiKeyAuthenticator.php
namespace App\Security;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;

class ApiKeyAuthenticator extends AbstractAuthenticator
{
    /**
     * Called on every request to decide if this authenticator should be
     * used for the request. Returning `false` will cause this authenticator
     * to be skipped.
     */
    public function supports(Request $request): ?bool
    {
        return $request->headers->has('X-AUTH-TOKEN');
    }

    public function authenticate(Request $request): Passport
    {
        $apiToken = $request->headers->get('X-AUTH-TOKEN');
        if (null === $apiToken) {
            // The token header was empty, authentication fails with HTTP Status
            // Code 401 "Unauthorized"
            throw new CustomUserMessageAuthenticationException('No API token provided');
        }

        return new SelfValidatingPassport(new UserBadge($apiToken));
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        // on success, let the request continue
        return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        $data = [
            // you may want to customize or obfuscate the message first
            'message' => strtr($exception->getMessageKey(), $exception->getMessageData())

            // or to translate this message
            // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
        ];

        return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
    }
}

The authenticator can be enabled using the custom_authenticators setting:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    # config/packages/security.yaml
    security:
        enable_authenticator_manager: true
    
        # ...
        firewalls:
            main:
                custom_authenticators:
                    - App\Security\ApiKeyAuthenticator
    
                # remember to also configure the entry_point if the
                # authenticator implements AuthenticationEntryPointInterface
                # entry_point: App\Security\CustomFormLoginAuthenticator
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <!-- 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">
            <!-- ... -->
    
            <!-- remember to also configure the entry-point if the
                 authenticator implements AuthenticatorEntryPointInterface
            <firewall name="main"
                entry-point="App\Security\CustomFormLoginAuthenticator"> -->
    
            <firewall name="main">
                <custom-authenticator>App\Security\ApiKeyAuthenticator</custom-authenticator>
            </firewall>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    // config/packages/security.php
    use App\Security\ApiKeyAuthenticator;
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        $security->enableAuthenticatorManager(true);
        // ....
    
        $security->firewall('main')
            ->customAuthenticators([ApiKeyAuthenticator::class])
    
            // remember to also configure the entry_point if the
            // authenticator implements AuthenticatorEntryPointInterface
            // ->entryPoint(App\Security\CustomFormLoginAuthenticator::class)
        ;
    };
    

The authenticate() method is the most important method of the authenticator. Its job is to extract credentials (e.g. username & password, or API tokens) from the Request object and transform these into a security Symfony\Component\Security\Http\Authenticator\Passport\Passport.

Tip

If you want to customize the login form, you can also extend from the Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator class instead.

Security Passports

New in version 5.2: The UserBadge was introduced in Symfony 5.2. Prior to 5.2, the user instance was provided directly to the passport.

A passport is an object that contains the user that will be authenticated as well as other pieces of information, like whether a password should be checked or if “remember me” functionality should be enabled.

The default Symfony\Component\Security\Http\Authenticator\Passport\Passport requires a user and credentials.

Use the Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge to attach the user to the passport. The UserBadge requires a user identifier (e.g. the username or email), which is used to load the user using the user provider:

use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;

// ...
$passport = new Passport(new UserBadge($email), $credentials);

Note

You can optionally pass a user loader as second argument to the UserBadge. This callable receives the $userIdentifier and must return a UserInterface object (otherwise a UserNotFoundException is thrown):

// src/Security/CustomAuthenticator.php
namespace App\Security;

use App\Repository\UserRepository;
// ...

class CustomAuthenticator extends AbstractAuthenticator
{
    private $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function authenticate(Request $request): Passport
    {
        // ...

        return new Passport(
            new UserBadge($email, function ($userIdentifier) {
                return $this->userRepository->findOneBy(['email' => $userIdentifier]);
            }),
            $credentials
        );
    }
}

The following credential classes are supported by default:

Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials

This requires a plaintext $password, which is validated using the password encoder configured for the user:

use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;

// ...
return new Passport(new UserBadge($email), new PasswordCredentials($plaintextPassword));
Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials

Allows a custom closure to check credentials:

use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;

// ...
return new Passport(new UserBadge($email), new CustomCredentials(
    // If this function returns anything else than `true`, the credentials
    // are marked as invalid.
    // The $credentials parameter is equal to the next argument of this class
    function ($credentials, UserInterface $user) {
        return $user->getApiToken() === $credentials;
    },

    // The custom credentials
    $apiToken
));

Self Validating Passport

If you don’t need any credentials to be checked (e.g. when using API tokens), you can use the Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport. This class only requires a UserBadge object and optionally Passport Badges.

Passport Badges

The Passport also optionally allows you to add security badges. Badges attach more data to the passport (to extend security). By default, the following badges are supported:

Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge
When this badge is added to the passport, the authenticator indicates remember me is supported. Whether remember me is actually used depends on special remember_me configuration. Read How to Add “Remember Me” Login Functionality for more information.
Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge
This is used to automatically upgrade the password to a new hash upon successful login. This badge requires the plaintext password and a password upgrader (e.g. the user repository). See How to Migrate a Password Hash.
Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge
Automatically validates CSRF tokens for this authenticator during authentication. The constructor requires a token ID (unique per form) and CSRF token (unique per request). See How to Implement CSRF Protection.
Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge
Indicates that this user was pre-authenticated (i.e. before Symfony was initiated). This skips the pre-authentication user checker.

New in version 5.2: Since 5.2, the PasswordUpgradeBadge is automatically added to the passport if the passport has PasswordCredentials.

For instance, if you want to add CSRF to your custom authenticator, you would initialize the passport like this:

// src/Service/LoginAuthenticator.php
namespace App\Service;

// ...
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;

class LoginAuthenticator extends AbstractAuthenticator
{
    public function authenticate(Request $request): Passport
    {
        $password = $request->request->get('password');
        $username = $request->request->get('username');
        $csrfToken = $request->request->get('csrf_token');

        // ... validate no parameter is empty

        return new Passport(
            new UserBadge($username),
            new PasswordCredentials($password),
            [new CsrfTokenBadge('login', $csrfToken)]
        );
    }
}

Tip

Besides badges, passports can define attributes, which allows the authenticate() method to store arbitrary information in the passport to access it from other authenticator methods (e.g. createAuthenticatedToken()):

// ...
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;

class LoginAuthenticator extends AbstractAuthenticator
{
    // ...

    public function authenticate(Request $request): Passport
    {
        // ... process the request

        $passport = new SelfValidatingPassport(new UserBadge($username), []);

        // set a custom attribute (e.g. scope)
        $passport->setAttribute('scope', $oauthScope);

        return $passport;
    }

    public function createToken(Passport $passport, string $firewallName): TokenInterface
    {
        // read the attribute value
        return new CustomOauthToken($passport->getUser(), $passport->getAttribute('scope'));
    }
}

New in version 5.2: Passport attributes were introduced in Symfony 5.2.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.