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.
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.
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!
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?
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.
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 |
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.
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!
// 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
}
}
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.