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 SubscribeOur registration form would work if we tried it. But, after registration, I want to also automatically authenticate the user... so they don't need to register and then immediately log in... that would be silly.
So far, all authentication has been done... kind of indirectly: the user makes a request, some authenticator handles it and... voilà ! But in this case, we want to authenticate the user directly, by writing code inside of a controller.
And... this is totally possible, by autowiring a service specifically for this. Add a new argument up here type-hinted with UserAuthenticatorInterface
and I'll call it $userAuthenticator
:
... lines 1 - 11 | |
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface; | |
class RegistrationController extends AbstractController | |
{ | |
... lines 16 - 18 | |
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserAuthenticatorInterface $userAuthenticator): Response | |
{ | |
... lines 21 - 48 | |
} | |
} |
This object allows you to just... authenticate any User
object. Right before the redirect, let's do that: $userAuthenticator->authenticateUser()
and we need to pass this a few arguments. The first one is the User
we want to authenticate:
... lines 1 - 13 | |
class RegistrationController extends AbstractController | |
{ | |
... lines 16 - 18 | |
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserAuthenticatorInterface $userAuthenticator): Response | |
{ | |
... lines 21 - 24 | |
if ($form->isSubmitted() && $form->isValid()) { | |
... lines 26 - 38 | |
$userAuthenticator->authenticateUser( | |
$user, | |
); | |
return $this->redirectToRoute('app_homepage'); | |
} | |
... lines 45 - 48 | |
} | |
} |
Easy. The second is an "authenticator" that you want to use. This system works by basically taking your User
object and... kind of "running it through" one of your authenticators.
If we were still using our custom LoginFormAuthenticator
, passing this argument would be really easy. We could just autowire the LoginFormAuthenticator
service up here and pass it in.
But, in our security.yaml
file, our main way of authenticating is form_login
:
security: | |
... lines 2 - 20 | |
firewalls: | |
... lines 22 - 24 | |
main: | |
... lines 26 - 28 | |
form_login: | |
login_path: app_login | |
check_path: app_login | |
username_parameter: email | |
password_parameter: password | |
enable_csrf: true | |
... lines 35 - 61 |
That does activate an authenticator service behind the scenes - just like our custom LoginFormAuthenticator
. The tricky part is figuring out what that service is and injecting it into our controller.
So, we need to do a bit of digging. At your terminal, run
symfony console debug:container
and search for form_login
:
symfony console debug:container form_login
In this list, I see a service called security.authenticator.form_login.main
... and remember that "main" is the name of our firewall. This is the id of the service that we want. If you're wondering about the service above this, if you checked, you'd find that it's an "abstract" service. A, sort of "fake" service that's used as a template to create the real service for any firewalls that use form_login
.
Anyways, I'll hit "1" to get more details. Ok cool: this service is an instance of FormLoginAuthenticator
, which is the core class that we looked at earlier.
Back in our controller, add another argument type-hinted with FormLoginAuthenticator
:
... lines 1 - 12 | |
use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator; | |
class RegistrationController extends AbstractController | |
{ | |
... lines 17 - 19 | |
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserAuthenticatorInterface $userAuthenticator, FormLoginAuthenticator $formLoginAuthenticator): Response | |
{ | |
... lines 22 - 51 | |
} | |
} |
Then, down here, pass the new argument to authenticateUser()
:
... lines 1 - 14 | |
class RegistrationController extends AbstractController | |
{ | |
... lines 17 - 19 | |
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserAuthenticatorInterface $userAuthenticator, FormLoginAuthenticator $formLoginAuthenticator): Response | |
{ | |
... lines 22 - 25 | |
if ($form->isSubmitted() && $form->isValid()) { | |
... lines 27 - 39 | |
$userAuthenticator->authenticateUser( | |
$user, | |
$formLoginAuthenticator, | |
... line 43 | |
); | |
... lines 45 - 46 | |
} | |
... lines 48 - 51 | |
} | |
} |
This won't work yet, but stick with me.
The final argument to authenticateUser()
is the Request
object... which we already have... it's our first controller argument:
... lines 1 - 14 | |
class RegistrationController extends AbstractController | |
{ | |
... lines 17 - 19 | |
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserAuthenticatorInterface $userAuthenticator, FormLoginAuthenticator $formLoginAuthenticator): Response | |
{ | |
... lines 22 - 25 | |
if ($form->isSubmitted() && $form->isValid()) { | |
... lines 27 - 39 | |
$userAuthenticator->authenticateUser( | |
$user, | |
$formLoginAuthenticator, | |
$request | |
); | |
... lines 45 - 46 | |
} | |
... lines 48 - 51 | |
} | |
} |
Done! Oh, and one cool thing about authenticateUser()
is that it returns a Response
object! Specifically, the Response
object from the onAuthenticationSuccess()
method of whatever authenticator we passed in. This means that instead of redirecting to the homepage, we can return this and, wherever that authenticator normally redirects to, we will redirect there as well, which could be the "target path".
Let's try this thing! Refresh the registration form to be greeted with... an awesome error!
Cannot autowire argument
$formLoginAuthenticator
.
Hmm. We did type-hint that argument with the correct class: FormLoginAuthenticator
:
... lines 1 - 12 | |
use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator; | |
class RegistrationController extends AbstractController | |
{ | |
... lines 17 - 19 | |
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserAuthenticatorInterface $userAuthenticator, FormLoginAuthenticator $formLoginAuthenticator): Response | |
{ | |
... lines 22 - 51 | |
} | |
} |
The problem is that this is a rare example of a service that is not available for autowiring! So, we need to configure this manually.
Fortunately, if we didn't already know what service we need, the error message gives us a great hint. It says:
... no such service exists, maybe you should alias this class to the existing
security.authenticator.form_login.main
service
Yup, it gave us the id of the service that we need to wire.
Go copy the argument name - formLoginAuthenticator
- and then open config/services.yaml
. Beneath _defaults
, add a new bind
called $formLoginAuthenticator
set to @
then... I'll go copy that long service id... and paste it here:
... lines 1 - 8 | |
services: | |
# default configuration for services in *this* file | |
_defaults: | |
... lines 12 - 13 | |
bind: | |
... line 15 | |
$formLoginAuthenticator: '@security.authenticator.form_login.main' | |
... lines 17 - 32 |
This says: whenever a service has a $formLoginAuthenticator
argument, pass it this service.
That... if we refresh... will get rid of our error.
Ok, let's finally register a new user! I'll use my real-life email... then any password... as long as it's 6 characters: our registration form came pre-built with that validation rule. And... we got it. Down on the web debug toolbar, we are logged in as Merlin! I feel the magical power.
Next: sometimes denying access is not as simple as just checking a role. For example, what if you had a question edit page and it needs to only be accessible to the creator of that question? It's time to discover a powerful system inside of Symfony called voters.
Hey Marc,
The UserAuthenticator
has a different responsibility than our custom LoginFormAuthenticator
. Its job is to actually log in users into the system. As you can see, we're not making our users go through the login form after registration, so the UserAuthenticator
knows nothing about how to authenticate a request (validate credentials, etc)
Cheers!
Hi there. What would you suggest for authentication with the WorkFlow component. The idea, in the registration form, user will provide some docs (id card...) and submit, the admin will then receive an notification and analyse the submitted item and decide to fully athenticate the user or not.
I'm planning just to add a new property to the User entity..like isValidated, boolean. Then, throught the WorkFlow component set that property to true when the admin accept the details sent throught the form. Is that correct !? Or what would you suggest?
Hey discipolat!
Sorry for the slow reply! That sounds very reasonable to me. If it's JUST this simple, I might not use the Workflow component, but that component is great, so I have no problems with it.
Does this help? Or did you also want to, somehow, authenticate the user the moment that the admin approves them?
Cheers!
The "Show Lines" feature seems to be broken in some code blocks on this page. For example try to load "... lines 1 - 11" in the first code block. Can you please fix this?
Hey @Guido
Sorry for that, We are working to solve this issue, soon this feature will come back!
Cheers and stay tuned!
At 4:32 I get the expected error "Cannot autowire ..." but it lacks the "Perhaps you should alias this class to..." part. Why is that?
Hey Matteo S.!
Hmm. Ok, here's how this works behind the scenes - it may help spot why you see the difference:
A) Symfony sees the type-hint for Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator that it can't autowire.
B) To give you the best error message, it then loops over every service in the container to see if any services have this class.
If a matching service (or services) ARE found, it'll give you that alias type-hint. If none are found, it won't. In this situation, I had the security.authenticator.form_login.main
service because I'm using the form_login
authenticator under my "main" firewall. So my first guess would be that you maybe aren't using form_login
under your firewall? If you are, then yes, I would also expect you to see the exact same message as me :).
Cheers!
Thank you! Indeed, turns out I'm not using form_login, but rather "custom_authenticator: App\Security\LoginFormAuthenticator" which I seem to remember was created by the Maker bundle.
Sweet! That explains it!
And yes, MakerBundle still generates a custom authenticator in all cases. I'm thinking we should change that by asking you a further question so we can determine if you really *need* a custom authenticator, or if we can just hook you up with form_login.
Cheers!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"babdev/pagerfanta-bundle": "^3.3", // v3.3.0
"composer/package-versions-deprecated": "^1.11", // 1.11.99.4
"doctrine/annotations": "^1.0", // 1.13.2
"doctrine/doctrine-bundle": "^2.1", // 2.6.3
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
"doctrine/orm": "^2.7", // 2.10.1
"knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
"knplabs/knp-time-bundle": "^1.11", // v1.16.1
"pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
"pagerfanta/twig": "^3.3", // v3.3.0
"phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
"scheb/2fa-bundle": "^5.12", // v5.12.1
"scheb/2fa-qr-code": "^5.12", // v5.12.1
"scheb/2fa-totp": "^5.12", // v5.12.1
"sensio/framework-extra-bundle": "^6.0", // v6.2.0
"stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
"symfony/asset": "5.3.*", // v5.3.4
"symfony/console": "5.3.*", // v5.3.7
"symfony/dotenv": "5.3.*", // v5.3.8
"symfony/flex": "^1.3.1", // v1.17.5
"symfony/form": "5.3.*", // v5.3.8
"symfony/framework-bundle": "5.3.*", // v5.3.8
"symfony/monolog-bundle": "^3.0", // v3.7.0
"symfony/property-access": "5.3.*", // v5.3.8
"symfony/property-info": "5.3.*", // v5.3.8
"symfony/rate-limiter": "5.3.*", // v5.3.4
"symfony/runtime": "5.3.*", // v5.3.4
"symfony/security-bundle": "5.3.*", // v5.3.8
"symfony/serializer": "5.3.*", // v5.3.8
"symfony/stopwatch": "5.3.*", // v5.3.4
"symfony/twig-bundle": "5.3.*", // v5.3.4
"symfony/ux-chartjs": "^1.3", // v1.3.0
"symfony/validator": "5.3.*", // v5.3.8
"symfony/webpack-encore-bundle": "^1.7", // v1.12.0
"symfony/yaml": "5.3.*", // v5.3.6
"symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
"twig/extra-bundle": "^2.12|^3.0", // v3.3.3
"twig/string-extra": "^3.3", // v3.3.3
"twig/twig": "^2.12|^3.0" // v3.3.3
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
"symfony/debug-bundle": "5.3.*", // v5.3.4
"symfony/maker-bundle": "^1.15", // v1.34.0
"symfony/var-dumper": "5.3.*", // v5.3.8
"symfony/web-profiler-bundle": "5.3.*", // v5.3.8
"zenstruck/foundry": "^1.1" // v1.13.3
}
}
Hi Ryan !
Another great tutorial.
I probably have a stupid question : What's the point of using
FormLoginAuthenticator $formLoginAuthenticator
authenticator when we could use our previousLoginFormAuthenticator
authenticator ?This would be something like :
Thanks,
Marc.