Firewalls & Authenticators
We built this log in form by making a route, controller and rendering a template:
| // ... lines 1 - 8 | |
| class SecurityController extends AbstractController | |
| { | |
| /** | |
| * @Route("/login", name="app_login") | |
| */ | |
| public function login(): Response | |
| { | |
| return $this->render('security/login.html.twig'); | |
| } | |
| } |
Dead simple. When we submit the form, it POSTs right back to /login. So, to authenticate the user, you might expect us to put some logic right here: like if this is a POST request, read the POSTed email & password, query for the User object... and eventually check the password. That makes perfect sense! And that is completely not what we're going to do.
Hello Firewalls
Symfony's authentication system works in a... bit of a magic way, which I guess is fitting for our site. At the start of every request, before Symfony calls the controller, the security system executes a set of "authenticators". The job of each authenticator is to look at the request, see if there is any authentication information that it understands - like a submitted email and password, or an API key that's stored on a header - and if there is, use that to query the user and check the password. If all that happens successfully then... boom! Authentication complete.
Our job is to write and activate these authenticators. Open up config/packages/security.yaml. Remember the two parts of security: authentication (who you are) and authorization (what you can do).
The most important part of this file is firewalls:
| security: | |
| // ... lines 2 - 13 | |
| firewalls: | |
| dev: | |
| pattern: ^/(_(profiler|wdt)|css|images|js)/ | |
| security: false | |
| main: | |
| lazy: true | |
| provider: app_user_provider | |
| # activate different ways to authenticate | |
| # https://symfony.com/doc/current/security.html#firewalls-authentication | |
| # https://symfony.com/doc/current/security/impersonating_user.html | |
| # switch_user: true | |
| // ... lines 27 - 33 |
A firewall is all about authentication: its job is to figure out who you are. And, it usually makes sense to have only one firewall in your app... even if there are multiple different ways to authenticate, like a login form and an API key and OAuth.
The "dev" Firewall
But... woh woh woh. If we almost always want only one firewall... why are there are already two? Here's how this works: at the start of each request, Symfony goes down the list of firewalls, reads the pattern key - which is a regular expression - and finds the first firewall whose pattern matches the current URL. So there's only ever one firewall active per request.
If you look closely, this first firewall is a fake! It basically matches if the URL starts with /_profiler or /_wdt... and then sets security to false:
| security: | |
| // ... lines 2 - 13 | |
| firewalls: | |
| dev: | |
| pattern: ^/(_(profiler|wdt)|css|images|js)/ | |
| security: false | |
| // ... lines 18 - 33 |
In other words, it's basically making sure that you don't create a security system that is so epically awesome that... you block the web debug toolbar and profiler.
So... in reality, we only have one real firewall called main. It has no pattern key, which means that it will match all requests that don't match the dev firewall. Oh, and the names of these firewalls - main and dev? They're totally meaningless.
Activating Authenticators
Most of the config that we're going to put beneath the firewall relates to activating authenticators: those things that execute early in each request and try to authenticate the user. We'll add some of that config soon. But these two top keys do something different. lazy allows the authentication system to not authenticate the user until it needs to and provider ties this firewall to the user provider we talked about earlier. You should have both of these lines... but neither are terribly important:
| security: | |
| // ... lines 2 - 13 | |
| firewalls: | |
| // ... lines 15 - 17 | |
| main: | |
| lazy: true | |
| provider: app_user_provider | |
| // ... lines 21 - 33 |
Creating a Custom Authenticator Class
Anyways, anytime that we want to authenticate the user - like when we submit a login form - we need an authenticator. There are some core authenticator classes that we can use, including one for login forms.... and I'll show you some of those later. But to start, let's build our own authenticator class from scratch.
To do that, go to terminal and run:
symfony console make:auth
As you can see, you can select "Login form authenticator" to cheat and generate a bunch of code for a login form. But since we're building things from scratch, select "Empty authenticator" and call it LoginFormAuthenticator.
Awesome. This did two things: it created a new authenticator class and also updated security.yaml. Open the class first: src/Security/LoginFormAuthenticator.php:
| // ... lines 1 - 11 | |
| class LoginFormAuthenticator extends AbstractAuthenticator | |
| { | |
| public function supports(Request $request): ?bool | |
| { | |
| // TODO: Implement supports() method. | |
| } | |
| public function authenticate(Request $request): PassportInterface | |
| { | |
| // TODO: Implement authenticate() method. | |
| } | |
| public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | |
| { | |
| // TODO: Implement onAuthenticationSuccess() method. | |
| } | |
| public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response | |
| { | |
| // TODO: Implement onAuthenticationFailure() method. | |
| } | |
| // ... lines 33 - 43 | |
| } |
The only rule about an authenticator is that it needs to implement AuthenticatorInterface... though usually you'll extend AbstractAuthenticator... which implements AuthenticatorInterface for you:
| // ... lines 1 - 8 | |
| use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; | |
| // ... lines 10 - 11 | |
| class LoginFormAuthenticator extends AbstractAuthenticator | |
| { | |
| // ... lines 14 - 43 | |
| } |
We'll talk about what these methods do one-by-one. Anyways, AbstractAuthenticator is nice because it implements a super boring method for you.
Once we activate this new class in the security system, at the beginning of every request, Symfony will call this supports() method and basically ask:
Do you see authentication information on this request that you understand?
To prove that Symfony will call this, let's just dd('supports'):
| // ... lines 1 - 11 | |
| class LoginFormAuthenticator extends AbstractAuthenticator | |
| { | |
| public function supports(Request $request): ?bool | |
| { | |
| dd('supports!'); | |
| } | |
| // ... lines 18 - 43 | |
| } |
Activating Authenticators with custom_authenticators
Okay, so how do we activate this authenticator? How do we tell our firewall that it should use our new class? Back in security.yaml, we already have the code that does that! This custom_authenticator line was added by the make:auth command:
| security: | |
| // ... lines 2 - 13 | |
| firewalls: | |
| // ... lines 15 - 17 | |
| main: | |
| // ... lines 19 - 20 | |
| custom_authenticator: App\Security\LoginFormAuthenticator | |
| // ... lines 22 - 34 |
So if you have a custom authenticator class, this is how you activate it. Later, we'll see that you can have multiple custom authenticators if you want.
Anyways, this means that our authenticator is already active! So let's try it. Refresh the login page. It hits the supports() method! In fact, if you go to any URL it will hit our dd(). On every request, before the controller, Symfony now asks our authenticator if it supports authentication on this request.
Next let's fill in the authenticator logic and get our user logged in!
22 Comments
Error does not added to use Passport
symfony console make:authwith selection 0 generate Empty authenticator does not generate use Passport by default.
Hey Maxim!
Ah, you're right! It only affects empty authenticator, with login form authenticator it's already there. I just created a PR in maker-bundle to fix it: https://github.com/symfony/... - feel free to give a review ;)
Cheers!
Hi,
Is there a possibility to call another authenticator after first fail? For example - after unsuccessful authentication from first (custom) authenticator to use internal method for checking user/pass in second authenticator? I defined 2 authenticators in security.yaml, both have defined entry point (start() method and implement AuthenticationEntryPointInterface) but it seams after fail in first it simply just showing authentication error. I wanted to authenticate user locally, if LDAP auth fails. Is that possible?
security:
Hey @Artur-M
What if use multiple user providers and not authenticators? There is a Chain user provider you can see configuration here https://symfony.com/doc/current/security/user_providers.html#chain-user-provider in most cases it should be enough because authenticator should not have lot of custom logic
Cheers!
Thanks, probably it can be solution!
Hi,
after bin/console make:auth
I get following error:
The service "security.command.debug_firewall" has a dependency on a non-existent service "App\Security\LoginFormAuthenticator".I'm using Symfony 5.4.11
console output:
Hey Stefan,
I believe you don't have enabled the autoconfigure feature. Do you have these lines in your
config/services.yamlfile?My config/services.yaml:
Yes I do. So it should work - but it does not?
What should I add to configure LoginFormAuthenticator ?
I also updated to Symfony 5.4.14 - same problem.
Ok, let's review a few things.
First: double-check that the file's name matches to the class name of your
LoginFormAuthenticatorSecond: Double-check its namespace, it should be
App\SecurityIf everything is ok, could you show me your
config/packages/security.yamlfile? There might be a hintHi MolloKhan,
thanks for your time and help...
filenames - check
namespace - check
doublecheck - check
-security.yaml - sure - appended afterwards.
It's a pimcore application where I try to extend security for some pages
I want to configure firewall: 'wir_beten_fw'
path '/intern/' is working and redirecting to 'beter_login'
some stuff from config.yaml
Your config looks good to me. For some reason your authenticator is not being auto-registered. Double-check in your
config/services.yamlif you're not excluding it under the key:Also try clearing the cache manually
rm -rf var/cacheSorry, no change
no exclude
after rm - no change
Don't know why - but this was missing in config/services.yaml:
so now - LoginFormAuthenticator - is found and executed
I can follow the tut
Thanks a lot
Ohh, so it was not excluded, but it was not been auto-registered. Makes sense :)
Hi
I have a question, how to secure form against changes value in hidden fields?
Hey Tomas,
Perhaps what you need here is a custom validation constraint. Here you can learn how to create own yourself https://symfony.com/doc/current/validation/custom_constraint.html
Cheers!
Hey
not, I'm looking for something what protect my form against "html" code manipulation
In cakephp is special component, maybe in symfony is something similarar
FormProtect
regards
It seems to me that that component does something similar to what you can achieve with custom validation constraints. The only difference is that you have to attach the validations to your entity fields.
Here you can learn how to create your own validators https://symfonycasts.com/screencast/symfony-forms/custom-validator
the tutorial is based on Symfony 4, but nothing meaningful has changed since then
Hi, if symfony calls the authenticator on each request, which is the reason for lazy parameter?
Hey GianlucaF,
Hm, yes, authenticator is called on each request... but we only call that supports() method on it, which returns true *only* on login route when we're sending a POST request to it, right? But if supports() returns false - we won't continue.
Cheers!
Great series! You guys are always having what I need atm! (that's the true magic)
Hey Maciej,
Thank you for your feedback! We're really happy to hear it was useful for you :)
Cheers!
"Houston: no signs of life"
Start the conversation!