Skip to content
  • About
    • What is Symfony?
    • Community
    • News
    • Contributing
    • Support
  • Documentation
    • Symfony Docs
    • Symfony Book
    • Screencasts
    • Symfony Bundles
    • Symfony Cloud
    • Training
  • Services
    • Platform.sh for Symfony Best platform to deploy Symfony apps
    • SymfonyInsight Automatic quality checks for your apps
    • Symfony Certification Prove your knowledge and boost your career
    • SensioLabs Professional services to help you with Symfony
    • Blackfire Profile and monitor performance of your apps
  • Other
  • Blog
  • Download
sponsored by
  1. Home
  2. Documentation
  3. Security
  4. How to Write a Custom Authenticator

How to Write a Custom Authenticator

Edit this page

Symfony comes with many authenticators and third party bundles also implement more complex cases like JWT and oAuth 2.0. However, sometimes you need to implement a custom authentication mechanism that doesn't exist yet or you need to customize one. In such cases, you must create and use your own authenticator.

Authenticators should implement the AuthenticatorInterface. You can also extend AbstractAuthenticator, which has a default implementation for the createToken() method that fits most use-cases:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 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
    {
        // "auth-token" is an example of a custom, non-standard HTTP header used in this application
        return $request->headers->has('auth-token');
    }

    public function authenticate(Request $request): Passport
    {
        $apiToken = $request->headers->get('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');
        }

        // implement your own logic to get the user identifier from `$apiToken`
        // e.g. by looking up a user in the database using its API key
        $userIdentifier = /** ... */;

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

    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);
    }
}

Tip

If your custom authenticator is a login form, you can extend from the AbstractLoginFormAuthenticator class instead to make your job easier.

The authenticator can be enabled using the custom_authenticators setting:

1
2
3
4
5
6
7
8
# config/packages/security.yaml
security:

    # ...
    firewalls:
        main:
            custom_authenticators:
                - App\Security\ApiKeyAuthenticator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 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">
            <custom-authenticator>App\Security\ApiKeyAuthenticator</custom-authenticator>
        </firewall>
    </config>
</srv:container>
1
2
3
4
5
6
7
8
9
10
11
12
// config/packages/security.php
use App\Security\ApiKeyAuthenticator;
use Symfony\Config\SecurityConfig;

return static function (SecurityConfig $security): void {
    $security->enableAuthenticatorManager(true);
    // ....

    $security->firewall('main')
        ->customAuthenticators([ApiKeyAuthenticator::class])
    ;
};

Tip

You may want your authenticator to implement AuthenticationEntryPointInterface. This defines the response sent to users to start authentication (e.g. when they visit a protected page). Read more about it in The Entry Point: Helping Users Start Authentication.

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 Passport (security passports are explained later in this article).

After the authentication process finished, the user is either authenticated or there was something wrong (e.g. incorrect password). The authenticator can define what happens in these cases:

onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response

If the user is authenticated, this method is called with the authenticated $token. This method can return a response (e.g. redirect the user to some page).

If null is returned, the request continues like normal (i.e. the controller matching the login route is called). This is useful for API routes where each route is protected by an API key header.

onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response

If an AuthenticationException is thrown during authentication, the process fails and this method is called. This method can return a response (e.g. to return a 401 Unauthorized response in API routes).

If null is returned, the request continues like normal. This is useful for e.g. login forms, where the login controller is run again with the login errors.

If you're using login throttling, you can check if $exception is an instance of TooManyLoginAttemptsAuthenticationException (e.g. to display an appropriate message).

Caution: Never use $exception->getMessage() for AuthenticationException instances. This message might contain sensitive information that you don't want to be publicly exposed. Instead, use $exception->getMessageKey() and $exception->getMessageData() like shown in the full example above. Use CustomUserMessageAuthenticationException if you want to set custom error messages.

Tip

If your login method is interactive, which means that the user actively logged into your application, you may want your authenticator to implement the InteractiveAuthenticatorInterface so that it dispatches an InteractiveLoginEvent

Security Passports

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 Passport requires a user and some sort of "credentials" (e.g. a password).

Use the 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:

1
2
3
4
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;

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

Note

The maximum length allowed for the user identifier is 4096 characters to prevent session storage flooding attacks.

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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/Security/CustomAuthenticator.php
namespace App\Security;

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

class CustomAuthenticator extends AbstractAuthenticator
{
    public function __construct(
        private UserRepository $userRepository,
    ) {
    }

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

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

The following credential classes are supported by default:

PasswordCredentials

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

1
2
3
4
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;

// ...
return new Passport(new UserBadge($email), new PasswordCredentials($plaintextPassword));
CustomCredentials

Allows a custom closure to check credentials:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\Component\Security\Core\User\UserInterface;
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 (string $credentials, UserInterface $user): bool {
        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 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:

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.
PasswordUpgradeBadge
This is used to automatically upgrade the password to a new hash upon successful login (if needed). This badge requires the plaintext password and a password upgrader (e.g. the user repository). See Password Hashing and Verification.
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.
PreAuthenticatedUserBadge
Indicates that this user was pre-authenticated (i.e. before Symfony was initiated). This skips the pre-authentication user checker.

Note

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 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;

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

        // ...

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

Passport Attributes

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. createToken()):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ...
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'));
    }
}
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version

    Symfony 7.1 is backed by

    Become certified from home

    Become certified from home

    Put the code quality back at the heart of your project

    Put the code quality back at the heart of your project

    Version:

    Table of Contents

    • Security Passports
      • Self Validating Passport
    • Passport Badges
    • Passport Attributes

    Symfony footer

    Avatar of Stéphane Escandell, a Symfony contributor

    Thanks Stéphane Escandell (@sescandell) for being a Symfony contributor

    2 commits • 317 lines changed

    View all contributors that help us make Symfony

    Become a Symfony contributor

    Be an active part of the community and contribute ideas, code and bug fixes. Both experts and newcomers are welcome.

    Learn how to contribute

    Symfony™ is a trademark of Symfony SAS. All rights reserved.

    • What is Symfony?

      • What is Symfony?
      • Symfony at a Glance
      • Symfony Components
      • Symfony Releases
      • Security Policy
      • Logo & Screenshots
      • Trademark & Licenses
      • symfony1 Legacy
    • Learn Symfony

      • Symfony Docs
      • Symfony Book
      • Reference
      • Bundles
      • Best Practices
      • Training
      • eLearning Platform
      • Certification
    • Screencasts

      • Learn Symfony
      • Learn PHP
      • Learn JavaScript
      • Learn Drupal
      • Learn RESTful APIs
    • Community

      • Symfony Community
      • SymfonyConnect
      • Events & Meetups
      • Projects using Symfony
      • Contributors
      • Symfony Jobs
      • Backers
      • Code of Conduct
      • Downloads Stats
      • Support
    • Blog

      • All Blog Posts
      • A Week of Symfony
      • Case Studies
      • Cloud
      • Community
      • Conferences
      • Diversity
      • Living on the edge
      • Releases
      • Security Advisories
      • Symfony Insight
      • Twig
      • SensioLabs Blog
    • Services

      • SensioLabs services
      • Train developers
      • Manage your project quality
      • Improve your project performance
      • Host Symfony projects

      Powered by

    Follow Symfony