Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Custom Authenticator authenticate() Method

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We're currently converting our old Guard authenticator to the new authenticator system. And, nicely, these two systems do share some methods, like supports(), onAuthenticationSuccess() and onAuthenticationFailure().

The big difference is down inside the new authenticate() method. In the old Guard system, we split up authentication into a few methods. We had getCredentials(), where we grab some information, getUser(), where we found the User object, and checkCredentials(), where we checked the password. All three of these things are now combined into the authenticate() method... with a few nice bonuses. For example, as you'll see in a second, it's no longer our responsibility to check the password. That now happens automatically.

The Passport Object

Our job in authenticate() is simple: to return a Passport. Go ahead and add a Passport return type. That's actually needed in Symfony 6. It wasn't added automatically due to a deprecation layer and the fact that the return type changed from PassportInterface to Passport in Symfony 5.4.

... lines 1 - 26
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
... lines 28 - 29
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
... lines 32 - 39
public function authenticate(Request $request): Passport
{
... lines 42 - 66
}
... lines 68 - 136
}

Anyways, this method returns a Passport... so do it: return new Passport(). By the way, if you're new to the custom authenticator system and want to learn more, check out our Symfony 5 Security tutorial where we talk all about this. I'll go through the basics now, but the details live there.

Before we fill in the Passport, grab all the info from the Request that we need... paste... then set each of these as variables: $email =, $password =... and let's worry about the CSRF token later.

... lines 1 - 39
public function authenticate(Request $request): Passport
{
$email = $request->request->get('email');
$password = $request->request->get('password');
return new Passport(
... lines 46 - 65
);
}
... lines 68 - 138

The first argument to the Passport is a new UserBadge(). What you pass here is the user identifier. In our system, we're logging in via the email, so pass $email!

And... if you want, you can stop right here. If you only pass one argument to UserBadge, Symfony will use the "user provider" from security.yaml to find that user. We're using an entity provider, which tells Symfony to try to query for the User object in the database via the email property.

Optional Custom User Query

In the old system, we did this all manually by querying the UserRepository. That is not needed anymore. But sometimes... if you have custom logic, you might still need to find the user manually.

If you have this use-case, pass a function() to the second argument that accepts a $userIdentifier argument. Now, when the authentication system needs the User object, it will call our function and pass us the "user identifier"... which will be whatever we passed to the first argument. So, the email.

Our job is to use that to return the user. Start with $user = $this->entityManager->getRepository(User::class)

And yea, I could have injected the UserRepository instead of the entity manager... that would be better... but this is fine. Then ->findOneBy(['email' => $userIdentifier]).

If we did not find a user, we need to throw a new UserNotFoundException(). Then, return $user.

... lines 1 - 39
public function authenticate(Request $request): Passport
{
... lines 42 - 44
return new Passport(
new UserBadge($email, function($userIdentifier) {
// optionally pass a callback to load the User manually
$user = $this->entityManager
->getRepository(User::class)
->findOneBy(['email' => $userIdentifier]);
if (!$user) {
throw new UserNotFoundException();
}
return $user;
}),
... lines 58 - 65
);
}
... lines 68 - 138

First Passport argument is done!

PasswordCredentials

For the second argument, down here, change my bad semicolon to a comma - then say new PasswordCredentials() and pass this the submitted $password.

... lines 1 - 39
public function authenticate(Request $request): Passport
{
... lines 42 - 44
return new Passport(
new UserBadge($email, function($userIdentifier) {
... lines 47 - 56
}),
new PasswordCredentials($password),
... lines 59 - 65
);
}
... lines 68 - 138

That's all we need! That's right: we do not need to actually check the password! We pass a PasswordCredentials()... and then another system is responsible for checking the submitted password against the hashed password in the database! How cool is that?

Extra Badges

Finally, the Passport accepts an optional array of "badges", which are extra "stuff" that you want to add... usually to activate other features.

We only need to pass one: a new CsrfTokenBadge(). This is because our login form is protected by a CSRF token. Previously, we checked that manually. Lame!

But no more! Pass the string authenticate to the first argument... which just needs to match the string used when we generate the token in the template: login.html.twig. If I search for csrf_token... there it is!

For the second argument, pass the submitted CSRF token: $request->request->get('_csrf_token'), which you can also see in the login form.

... lines 1 - 39
public function authenticate(Request $request): Passport
{
... lines 42 - 44
return new Passport(
... lines 46 - 57
new PasswordCredentials($password),
[
new CsrfTokenBadge(
'authenticate',
$request->request->get('_csrf_token')
),
... line 64
]
);
}
... lines 68 - 138

And... done! Just by passing the badge, the CSRF token will be validated.

Oh, and while we don't need to do this, I'm also going to pass a new RememberMeBadge(). If you use the "Remember Me" system, then you need to pass this badge. It tells the system that you opt "into" having a remember me cookie set if the user logs in using this authenticator. But you still need to have a "Remember Me" checkbox here... for it to work. Or, to always enable it, add ->enable() on the badge.

... lines 1 - 39
public function authenticate(Request $request): Passport
{
... lines 42 - 44
return new Passport(
... lines 46 - 57
new PasswordCredentials($password),
[
... lines 60 - 63
(new RememberMeBadge())->enable(),
]
);
}
... lines 68 - 138

And, of course, none of this will work unless you activate the remember_me system under your firewall, which I don't actually have yet. It's still safe to add that badge... but there won't be any system to process it and add the cookie. So, the badge will be ignored.

Deleting Old Methods!

Anyways, we're done! If that felt overwhelming, back up and watch our Symfony Security tutorial to get more context.

The cool thing is that we don't need getCredentials(), getUser(), checkCredentials(), or getPassword() anymore. All we need is authenticate(), onAuthenticationSuccess(), onAuthenticationFailure(), and getLoginUrl(). We can even celebrate up here by removing a bunch of old use statements. Yay!

Oh, and look at the constructor. We no longer need CsrfTokenManagerInterface or UserPasswordHasherInterface: both of those checks are now done somewhere else. And... that gives us two more use statements to delete.

... lines 1 - 28
public function __construct(private SessionInterface $session, private EntityManagerInterface $entityManager, private UrlGeneratorInterface $urlGenerator)
{
}
... lines 32 - 87

Activating the New Security System

At this point, our one custom authenticator has been moved to the new authenticator system. This mean that, in security.yaml, we are ready to switch to the new system! Say enable_authenticator_manager: true.

security:
... lines 2 - 9
enable_authenticator_manager: true
... lines 11 - 64

And these custom authenticators aren't under a guard key anymore. Instead, add custom_authenticator and add this directly below that.

security:
... lines 2 - 20
firewalls:
... lines 22 - 24
main:
... lines 26 - 27
custom_authenticator:
- App\Security\LoginFormAuthenticator
... lines 30 - 63

Okay, moment of truth! We just completely switched to the new system. Will it work? Head back to the homepage, reload and... it does! And check out those deprecations! It went from around 45 to 4. Woh!

Some of those relate to one more security change. Next: let's update to the new password_hasher & check out a new command for debugging security firewalls.

Leave a comment!

2
Login or Register to join the conversation
Tien dat L. Avatar
Tien dat L. Avatar Tien dat L. | posted 3 months ago

Hi, in Symfony 5.4 I can use "Abstract Guard Authenticator" to handle authentication with SSO, but this class is deprecated. Is there an alternative in Symfony 6.1 ?

Reply

Hey Tien dat L.

You should just migrate from Guard authentication to standard security authentication, it's very close to Guard, but more flexible and straightforward.

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.6", // v3.6.1
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.5
        "doctrine/annotations": "^1.13", // 1.13.2
        "doctrine/dbal": "^3.3", // 3.3.5
        "doctrine/doctrine-bundle": "^2.0", // 2.6.2
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.0", // 2.11.2
        "knplabs/knp-markdown-bundle": "^1.8", // 1.10.0
        "knplabs/knp-time-bundle": "^1.18", // v1.18.0
        "pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
        "pagerfanta/twig": "^3.6", // v3.6.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.6
        "sentry/sentry-symfony": "^4.0", // 4.2.8
        "stof/doctrine-extensions-bundle": "^1.5", // v1.7.0
        "symfony/asset": "6.0.*", // v6.0.7
        "symfony/console": "6.0.*", // v6.0.7
        "symfony/dotenv": "6.0.*", // v6.0.5
        "symfony/flex": "^2.1", // v2.1.7
        "symfony/form": "6.0.*", // v6.0.7
        "symfony/framework-bundle": "6.0.*", // v6.0.7
        "symfony/mailer": "6.0.*", // v6.0.5
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/property-access": "6.0.*", // v6.0.7
        "symfony/property-info": "6.0.*", // v6.0.7
        "symfony/proxy-manager-bridge": "6.0.*", // v6.0.6
        "symfony/routing": "6.0.*", // v6.0.5
        "symfony/runtime": "6.0.*", // v6.0.7
        "symfony/security-bundle": "6.0.*", // v6.0.5
        "symfony/serializer": "6.0.*", // v6.0.7
        "symfony/stopwatch": "6.0.*", // v6.0.5
        "symfony/twig-bundle": "6.0.*", // v6.0.3
        "symfony/ux-chartjs": "^2.0", // v2.1.0
        "symfony/validator": "6.0.*", // v6.0.7
        "symfony/webpack-encore-bundle": "^1.7", // v1.14.0
        "symfony/yaml": "6.0.*", // v6.0.3
        "symfonycasts/verify-email-bundle": "^1.7", // v1.10.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.8
        "twig/string-extra": "^3.3", // v3.3.5
        "twig/twig": "^2.12|^3.0" // v3.3.10
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.1
        "phpunit/phpunit": "^9.5", // 9.5.20
        "rector/rector": "^0.12.17", // 0.12.20
        "symfony/debug-bundle": "6.0.*", // v6.0.3
        "symfony/maker-bundle": "^1.15", // v1.38.0
        "symfony/var-dumper": "6.0.*", // v6.0.6
        "symfony/web-profiler-bundle": "6.0.*", // v6.0.6
        "zenstruck/foundry": "^1.16" // v1.18.0
    }
}