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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe'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.
8 Comments
Oh man, but why these changes? The old security system methods seemed fine and suitable. Now it looks o er simplified. Is flexibility lost? It looks like readability is reduced.
I think I'm getting the hang of the new security component but stuck on where to do somethings. Our application uses LDAP for authentication which has been successfully moved into CustomCrendentials - no problem there. However, we don't require a user be in the database to successfully authenticate. In Guard Authenticator, we added the user to the database upon successful authentication. The local database is only used to track first names and privilege levels. I see I can use a custom function when a new UserBadge is created where I think I can create and return a new user if the user isn't already in the database. However, how can I access this new user in onAuthenticationSuccess to then persist it to the database? Can I add the passport as an input argument to onAuthenticationSuccess?
I solved this by moving the LDAP lookup and authentication into authenticate rather than doing it in onAuthenticationSuccess. I also had to move adding a new user to the database into authenticate. In my case I could not create a new user when creating a new passport as the user would never get successfully refreshed after adding the user to the database. It worked fine for existing user but not for new ones. So I had to do the LDAP search first, then authenticate the user's password, then add the user to the database and lastly, create the passport and return it.
Hey @CMarcus ,
Woh, that's a complex issue, I'm happy to hear you were able to workaround it. I personally don't use LDAP so it's hard to suggest you a better solution, but IMO this looks solid. I would recommend you to double-check the flows with new/existent users, especially look for possible errors and make sure they all are no edge cases and those are handled well with your solution.
You still may leverage
onAuthenticationSuccess()for non-critical actions like logging, tracking login timestamps, or redirecting users, etc. but keeping the main logic inauthenticate()should do the trick for you, well done.I hope this helps! and thanks for sharing the solution with others. It's definitely useful for our LDAP friends, probably some will leave you feedback or share their own ideas as well.
Cheers!
I also don't like these changes, like Cameron. So much happens automatically under the hood. It's difficult to know what is going on. Now I am not sure how to refactor this, as I'm working on a bigger application with a lot of customisations.
Hey @rzrztrzrtz
I feel you, the change feels overwhelming at the beginning but it was for good. The security component is easier/simpler to use and is designed as an event-based system, which is nice because you can now customize it easily, for example, adding 2FA protection is not a problem at all.
I hope this course helps you upgrade your application without much hassle
Cheers!
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 ?
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!
"Houston: no signs of life"
Start the conversation!