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 SubscribeNow that we've added our authenticator under the authenticators
key:
security: | |
... lines 2 - 8 | |
firewalls: | |
... lines 10 - 12 | |
main: | |
... lines 14 - 15 | |
guard: | |
authenticators: | |
- App\Security\LoginFormAuthenticator | |
... lines 19 - 33 |
Symfony calls its supports()
method at the beginning of every request, which is why we see this little die statement:
... lines 1 - 10 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
public function supports(Request $request) | |
{ | |
die('Our authenticator is alive!'); | |
} | |
... lines 17 - 41 | |
} |
These authenticator classes are really cool because each method controls just one small part of the authentication process.
supports()
MethodThe first method - supports()
- is called on every request. Our job is simple: to return true
if this request contains authentication info that this authenticator knows how to process. And if not, to return false
.
In this case, when we submit the login form, it POSTs to /login
. So, our authenticator should only try to authenticate the user in that exact situation. Return $request->attributes->get('_route') === 'app_login'
:
... lines 1 - 10 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
public function supports(Request $request) | |
{ | |
// do your work when we're POSTing to the login page | |
return $request->attributes->get('_route') === 'app_login' | |
... line 17 | |
} | |
... lines 19 - 43 | |
} |
Let me... explain this. If you look in SecurityController
, the name of our login route is app_login
:
... lines 1 - 8 | |
class SecurityController extends AbstractController | |
{ | |
/** | |
* @Route("/login", name="app_login") | |
*/ | |
public function login(AuthenticationUtils $authenticationUtils) | |
{ | |
... lines 16 - 25 | |
} | |
} |
And, though you don't need to do it very often, if you want to find out the name of the currently-matched route, you can do that by reading this special _route
key from the request attributes. In other words, this is checking to see if the URL is /login
. We also only want our authenticator to try to login the user if this is a POST request. So, add && $request->isMethod('POST')
:
... lines 1 - 10 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
public function supports(Request $request) | |
{ | |
// do your work when we're POSTing to the login page | |
return $request->attributes->get('_route') === 'app_login' | |
&& $request->isMethod('POST'); | |
} | |
... lines 19 - 43 | |
} |
Here's how this works: if we return false
from supports()
, nothing else happens. Symfony doesn't call any other methods on our authenticator, and the request continues on like normal to our controller, like nothing happened. It's not an authentication failure - it's just that nothing happens at all.
If we return true
from supports()
, well, that's when the fun starts. If we return true
, Symfony will immediately call getCredentials()
:
... lines 1 - 10 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 13 - 19 | |
public function getCredentials(Request $request) | |
{ | |
... line 22 | |
} | |
... lines 24 - 43 | |
} |
To see if things are working, let's just dump($request->request->all())
, then die()
:
... lines 1 - 10 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 13 - 19 | |
public function getCredentials(Request $request) | |
{ | |
dump($request->request->all());die; | |
} | |
... lines 24 - 43 | |
} |
I know, that looks funny. Unrelated to security, if you want to read POST data off of the request, you use the $request->request
property.
Anyways, let's try it! Go back to your browser and hit enter on the URL so that it makes a GET
request to /login
. Hello login page! Our supports()
method just returned false
. And so, the request continued anonymously, like normal.
Log in with one of our dummy users: spacebar1@example.com
. The password doesn't matter. And... enter! Yes! This time, because this is a POST request to /login
, supports()
returns true
! So, Symfony calls getCredentials()
and our dump fires! As expected, we can see the email
and password
POST parameters, because the login form uses these names:
... lines 1 - 10 | |
{% block body %} | |
<form class="form-signin" method="post"> | |
... lines 13 - 18 | |
<input type="email" name="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus> | |
... line 20 | |
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required> | |
... lines 22 - 29 | |
</form> | |
{% endblock %} |
dd()
FunctionOh, and I want to show you a quick new Easter egg in Symfony 4.1, unrelated to security. Instead of dump()
and die
, use dd()
and then remove the die
:
... lines 1 - 10 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 13 - 19 | |
public function getCredentials(Request $request) | |
{ | |
dd($request->request->all()); | |
} | |
... lines 24 - 43 | |
} |
Refresh! Same result. This is just a nice, silly shortcut: dd()
is dump()
and die
. We'll use it... because... why not?
getCredentials()
MethodBack to work! Our job in getCredentials()
is simple: to read our authentication credentials off of the request and return them. In this case, we'll return the email
and password
. But, if this were an API token authenticator, we would return that token. We'll see that later.
Return an array with an email
key set to $request->request->get('email')
and password
set to $request->request->get('password')
:
... lines 1 - 10 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 13 - 19 | |
public function getCredentials(Request $request) | |
{ | |
return [ | |
'email' => $request->request->get('email'), | |
'password' => $request->request->get('password'), | |
]; | |
} | |
... lines 27 - 46 | |
} |
I'm just inventing these email
and password
keys for the new array: we can really return whatever we want from this method. Because, after we return from getCredentials()
, Symfony will immediately call getUser()
and pass this array back to us as the first $credentials
argument:
... lines 1 - 10 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 13 - 27 | |
public function getUser($credentials, UserProviderInterface $userProvider) | |
{ | |
... line 30 | |
} | |
... lines 32 - 46 | |
} |
Let's see that in action: dd($credentials)
:
... lines 1 - 10 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 13 - 27 | |
public function getUser($credentials, UserProviderInterface $userProvider) | |
{ | |
dd($credentials); | |
} | |
... lines 32 - 46 | |
} |
Move back to your browser and, refresh! Coincidentally, it dumps the exact same thing as before. But, this time, it's coming from line 30 - our line in getUser()
.
getUser()
MethodLet's keep going! Our job in getUser()
is to use these $credentials
to return a User
object, or null if the user isn't found. Because we're storing our users in the database, we need to query for the user via their email. And to do that, we need the UserRepository
that was generated with our entity.
At the top of the class, add public function __construct()
with a UserRepository $userRepository
argument:
... lines 1 - 4 | |
use App\Repository\UserRepository; | |
... lines 6 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 14 - 15 | |
public function __construct(UserRepository $userRepository) | |
{ | |
... line 18 | |
} | |
... lines 20 - 54 | |
} |
I'll hit Alt
+Enter
and select "Initialize Fields" to add that property and set it:
... lines 1 - 4 | |
use App\Repository\UserRepository; | |
... lines 6 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
private $userRepository; | |
public function __construct(UserRepository $userRepository) | |
{ | |
$this->userRepository = $userRepository; | |
} | |
... lines 20 - 54 | |
} |
Back down in getUser()
, just return $this->userRepository->findOneBy()
to query by email, set to $credentials['email']
:
... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 14 - 35 | |
public function getUser($credentials, UserProviderInterface $userProvider) | |
{ | |
return $this->userRepository->findOneBy(['email' => $credentials['email']]); | |
} | |
... lines 40 - 54 | |
} |
This will return our User
object, or null
. The cool thing is that if this returns null
, the whole authentication process will stop, and the user will see an error. But if we return a User
object, then Symfony immediately calls checkCredentials()
, and passes it the same $credentials
and the User
object we just returned:
... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 14 - 40 | |
public function checkCredentials($credentials, UserInterface $user) | |
{ | |
... line 43 | |
} | |
... lines 45 - 54 | |
} |
Inside, dd($user)
so we can see if things are working:
... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 14 - 40 | |
public function checkCredentials($credentials, UserInterface $user) | |
{ | |
dd($user); | |
} | |
... lines 45 - 54 | |
} |
Refresh and... got it! That's our User
object!
checkCredentials()
MethodOk, final step: checkCredentials()
. This is your opportunity to check to see if the user's password is correct, or any other last, security checks. Right now... well... we don't have a password, so, let's return true
:
... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 14 - 40 | |
public function checkCredentials($credentials, UserInterface $user) | |
{ | |
// only needed if we need to check a password - we'll do that later! | |
return true; | |
} | |
... lines 46 - 55 | |
} |
And actually, in many systems, simply returning true
is perfect! For example, if you have an API token system, there's no password.
If you did return false
, authentication would fail and the user would see an "Invalid Credentials" message. We'll see that soon.
But, when you return true... authentication is successful! Woo! To figure out what to do, now that the user is authenticated, Symfony calls onAuthenticationSuccess()
:
... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 14 - 46 | |
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) | |
{ | |
... line 49 | |
} | |
... lines 51 - 55 | |
} |
Put a dd()
here that says "Success":
... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 14 - 46 | |
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) | |
{ | |
dd('success!'); | |
} | |
... lines 51 - 55 | |
} |
Move over and... refresh the POST! Yes! We hit it! At this point, we have fully filled in all the authentication logic. We used supports()
to tell Symfony whether or not our authenticator should be used in this request, fetched credentials off of the request, used those to find the user, and returned true
in checkCredentials()
because we don't have a password.
Next, let's fill in these last two methods and finally see - for real - that our user is logged in. We'll also learn a bit more about what happens when authentication fails and how the error message is rendered.
I have ended up with the following: FYI:
public function getCredentials(Request $request)
{
$form = $this->formFactory->create(LoginType::class);
$form->handleRequest($request);
$validation_errors = $this->validator->validate($form);
foreach ($validation_errors as $error)
{
if(self::str_contains($error->getPropertyPath(), 'email'))
{
throw new CustomUserMessageAuthenticationException(
$this->translator->trans('You entered an invalid email address.')
);
}
if(self::str_contains($error->getPropertyPath(), 'password'))
{
throw new CustomUserMessageAuthenticationException(
$this->translator->trans('Your password must be at least 8 characters.')
);
}
}
$data = $form->getData();
$post = $request->request->all();
$credentials = [
'email' => $data['email'],
'password' => $data['password'],
'csrf_token' => $post['login']['_csrf_token'],
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['email']
);
return $credentials;
}
Hey Yahya E.!
Ah! Your second solution works. But you first solution was SO close. The change that got you was something that changed from Symfony 3 to Symfony 4. Here is what you needed in the first code:
if($form->isSubmitted() && $form->isValid())
That's it! The key is the $form->isSubmitted()
. That line doesn't make as much sense in this context, but basically, Symfony wants you to also make sure the form was submitted (i.e. that this wasn't a GET request) before actually asking if it's valid.
I hope it will help simplify your code a bit!
Cheers!
Does it has any security benefits to use SF form over our standard html form, which would make the more work worth? (e.g. better protection against xss or mysql injection?)
Hey Mike,
Unfortunately, any forms do not protect you from MySQL injection, your MySQL client should worry about it. And Doctrine do so if you use it the correct way, i.e. use parameters instead of concatenating dynamic values to query strings. You can check our Doctrine tutorial about how to write good queries: https://symfonycasts.com/sc... .
What about XSS - yes! Symfony Forms has CSRF protection enabled by default. You can turn it of in global settings or turn off in specific forms. To ensure you have CSRF protection in Symfony Forms - you can render the form and look for hidden field with token.
And fairly speaking, even if we're talking about simple forms, it's more convenient to work with Symfony Forms instead of handle them manually. Well, you can do an experiment, implement a form with Symfony Form and then do the same with custom form and compare results.
Cheers!
Any explanation why start() functioon is fired even that support return false, normally the authenticator should be skipped ?
Hello ahmedbhs
start()
method is a part of AuthenticationEntryPointInterface
it helps to define which way user should be authenticated if you have multiple authenticators. You can read more about it here https://symfony.com/doc/current/security/entry_point.html
Cheers!
I am new to Symfony web application development. And at the moment Iam building a web application to get my head around Symfony web development. I have stored some sensitive keys in the Symfony's secrets management system (vault). And I want to access a key in the vault to do some operation. I need to access the vault items in the LoginFormAuthenticator class which extends AbstractFormLoginAuthenticator, and $this->getParameter('key_name') is not working in the LoginFormAuthenticator class.
Is there a work around where I should be able to access the vault items in that class ?
Hey Chamal,
To access an environment variable in PHP user $_ENV or $_SERVER vars, i.e. you can get that "KEY_NAME" env var via $_ENV['KEY_NAME']
or $_SERVER['KEY_NAME']. Usually, env vars are written in uppercase.
Or, if you want to get it via getParameter() instead - you need to create the "parameter" first. For example, in config/services.yaml add:
parameters:
key_name: '%env(KEY_NAME)%'
And then in your PHP code call "->getParameter('key_name')" that you just created.
I hope this helps!
Cheers!
Hi Victor,
Thanks for your reply.
That is what I have exactly done it, in my code. Pasted the code below for more clarity, along with the error message I received.
in config/services.yaml
parameters:
db_access_key: '%env(DB_ACCESS_KEY)%'
db_access_secret: '%env(DB_ACCESS_SECRET)%'
In LoginFormAuthenticator
public function getUser($credentials, UserProviderInterface $userProvider) {
$email = $credentials['email'];
dump($this->getParameter('db_access_secret')); //error on this line
}
And I get the following exception:
<blockquote>Attempted to call an undefined method named "getParameter" of class "App\Security\LoginFormAuthenticator".
</blockquote>
Hey Chamal,
Ah, I see your problem now :) Sure, the "getParameter()" method is available in your controllers only, and only in case you extends Symfony base controller. If you want to get a parameter in your service - you have to inject it there, and that's a good practice btw!
So, the easiest way will be next: in your config/services.yaml, create a bind for a variable name and associated parameter:
services:
_defaults:
bind:
$dbAccessKey: '%db_access_key%'
And then in your "LoginFormAuthenticator" add the constructor if you don't have it, that will look like this:
class LoginFormAuthenticator
{
private $dbAccessKey;
public function __construct($dbAccessKey)
{
$this->dbAccessKey = $dbAccessKey
}
}
I.e., now you can ask for $dbAccessKey parameter injection in any service this way. The same way do for other parameters you need to inject.
Another way would be to inject the Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface service into the LoginFormAuthenticator that will give you access to all the parameters via ->get('db_access_key'). It might be useful when you need to get access to many parameters in your service. But unless you need just one or a few - the 1st way is recommended.
I hope this helps!
Cheers!
Hi Victor,
Thanks for your replying.
I have already solved my issue, and it was the solution that you have suggested second. I have injected the ContainerBagInterface to the LoginFormAuthenticator class, and it worked. I have tried your first solution as well, but it didnt work for me. Maybe because I have configured it incorrectly. I will try out again.
Thank you very much again for helping to resolve my issue.
Cheers !
Hey Chamal,
Thanks for letting me know the 2nd solution works for you. Yeah, it may also depends on the version of Symfony you're using in your project. Anyway, if the 2nd works for you well - great!
Cheers!
Hello there !
UserProvider is passed to getUser() but not used in your example.
Just wondering in what cases UserProvider would be useful in getUser().
Thanks !
Hey Nicolas-S!
Wonderful question! I have an answer for it back here - https://symfonycasts.com/sc... - let me know if it helps :).
Cheers!
Hey! In recent months, there has been a new way to manage authentication with Symfony: https://symfony.com/doc/cur...
It uses the new generation of GUARD if I understood correctly. But that's not all, by setting up this new system, we could use the "new" component of Symfony: The login throttling ( https://symfony.com/blog/ne... ) based on the RateLimiter.
I went through the doc 'to try to set up the new authentication system, based on our LoginFormAuthenticator, but it is much more complicated than expected, the doc is not very clear and goes in all meaning. In the end, we don't really know what we should use, which class to extend or implement, which function to perform and how. This is a drawback ...
Have you planned to make an Update video about this new component?
(And why not create a new command to set up this new authentication mode, like the php bin/console make:auth command)
Hey @kieuga!
Yes! I know and love this system - I’ve been working with Wouter... who has done ALL the real work on it - to perfect it.
A few things:
1) we will do a tutorial, but I’m waiting for symfony 5.3, as a few more (nice) changes are coming that I want to show (the splitting up of UserInterface and renaming of getUsername).
2) version 1.26.0 of maker bundle - https://github.com/symfony/... - was updated to support the new system :). It detects if you have it enabled and generates different code. We would like to add more options to that command, but it supports the current empty and form login authenticators.
If you have any questions about the new system before 5.3, I’d be happy to answer them :).
Cheers!
It's just awesome weaverryan Good job! For my part, I continued to reform this new authentication system (but very good news that the maker will be able to manage this soon!). I finally succeeded.
On the other hand, I don't know if you will talk about it in your tutorial or even if the maker will handle it, but regarding the Login Throttling, it does not work at all for me. Finally, I think this is mostly because I'm on Windows, and Semaphore is not supported for the locker. So I opted for "Flock" for the locker by simply replacing LOCK_DSN=semaphore
by LOCK_DSN=flock
in my <b>.env</b>, but it's like it's being ignored, like if I had not implemented this new feature. Weird. Hope your tutorial will talk about its use for us recalcitrant Windows lovers! :D
Hey Kiuega!
Woohoo - nice work!
but regarding the Login Throttling, it does not work at all for me
Hmm. Indeed, I will have to keep this in mind. When you use the login_throttling
under your firewall, it crates a basic rate limiting service to use. But there is also an option to use a custom, rate limiter service id.
So here is what I bet is happening and what you should do:
1) Configure a custom rate limiter - via the framework.rate_limiter config to use Flock. It sounds like you've already done this. If you named it "login_throttler" in the config file, then behind the scenes, this is creating a service with the id "limiter. login_throttler".
2) Specify this in your security config:
security:
firewalls:
default:
login_throttling:
limiter: limiter. login_throttler
I'm doing some guess work here. There are some holes in the docs (lock is documented, rate limiter is documented and login throttling is documented, but this crosses all 3).
Btw, if you want to do any debugging, here is the class that's called on login and does the rate limiting logic: https://github.com/symfony/symfony/blob/5.x/src/Symfony/Component/Security/Http/EventListener/LoginThrottlingListener.php
Let me know what you find out :).
Cheers!
Grrr my previous answer was marked as spam. I am enraged at having to start my answer all over again. :x
Hey weaverryan and thanks for your answer !
So I followed your directions, and using the doc ', I configured everything as needed.
I kept in the <b>.env</b> my LOCK_DSN=flock
to be able to use flock all the time without reconfiguring it each time.
I created my custom <b>rate_limiter</b>:
framework:
rate_limiter:
login_throttler:
policy: 'fixed_window'
limit: 2
interval: '2 minutes'
<blockquote>
Autowirable Types
=================
The following classes & interfaces can be used as type-hints when autowiring:
(only showing classes/interfaces matching limiter)
Symfony\Component\RateLimiter\RateLimiterFactory $loginThrottlerLimiter (limiter.login_throttler)
</blockquote>
<
login_throttling:
limiter: limiter.login_throttler
In fact, I can still try as many times as I want with the wrong password. However, if I try with a good password this time around I get this:
<blockquote>
Argument 2 passed to Symfony\Component\Security\Http\EventListener\LoginThrottlingListener::__construct() must be an instance of Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface, instance of Symfony\Component\RateLimiter\RateLimiterFactory given, called in C:\Users\user\Desktop\Sylvia\var\cache\dev\ContainerWv8DSJy\getSecurity_Listener_LoginThrottling_MainService.php on line 38
</blockquote>
I don't understand, the documentation tells us that we can use a custom rate_limiter for this feature, but doesn't seem to support it. Or am I doing something wrong?
Hey Kiuega!
First, apologies for my slow reply - it was crazy week :p.
Grrr my previous answer was marked as spam. I am enraged at having to start my answer all over again. :x
Really sorry about that :/. We DO find and rescue these... not that this helps you for this time :/
Ok, let's get to your question! There are 2 interesting pieces. And let me first say that you followed my instructions beautifully. But... I apparently did not lead you down the path all the way correctly ;).
A) Argument 2 passed to LoginThrottlingListener::__construct() must be an instance of RequestRateLimiterInterface, instance of RateLimiterFactory given
I've just asked the author of this feature about this. There is a little disconnect that I didn't realize. Basically, the framework.rate_limiter
config allows you to create RateLimiterFactory... but what the login throttling system needs is a RequestRateLimiterInterface... which is kind of a class that wraps the actual "rate limiters". Right now, I don't see a simple way of actually creating this. I mean, it's possible, but much harder than it should be. I'm either missing a detail (very possible), or this was an oversight. That's why I've asked the author. I will let you know :).
If you want to create the service yourself, create a new service in services.yaml that looks like this:
services:
# ...
app.default_login_rate_limiter:
class: Symfony\Component\Security\Http\RateLimiter \DefaultLoginRateLimiter
arguments: ['@limiter.login_throttler', '@limiter.login_throttler']
Then pass this app.default_login_rate_limiter
as your limiter in security.yaml.
B) Why did you not see the "LoginThrottlingListener::__construct() must be an instance" when you typed an invalid password?
This part... I can't explain. Basically, after you followed my directions, your rate limiter was totally misconfigured. This means that you should have gotten an error regardless of whether your typed in your password correctly or incorrectly: the rate limiter should have been used in both cases.
When I tried this locally (just now), I DID get the error regardless of whether I typed in the right password or wrong password. So something in your setup is different. I'm using the generic login form that you get with make:auth from MakerBundle. What does your authenticator look like?
Cheers!
Hi Ryan! No worries I understand that you are very busy, thank you for taking time for me it's nice!
Okay so small change compared to the last time at my place. I installed the dual-boot to be able to develop on Ubuntu (yeeees awesome) and logically be able to use semaphore naturally.
<blockquote>When I tried this locally (just now), I DID get the error regardless of whether I typed in the right password or wrong password. So something in your setup is different. I'm using the generic login form that you get with make:auth from MakerBundle. </blockquote>
The answer below (the one crossed out) is totally wrong, ignore it. The real answer is just below what is crossed out.
<s>What you said there disturbed me. If in doubt, I also recreated a new project (EDIT: no I took an old project which was still virgin, and the rest of this text is therefore incorrect. I'm editing, I edit this post as soon as I'm done), and I set up the authentication with the make: auth command (so by generating a guard authentication, that is to say, the future old version), and I configured a login_throttler in the security.yaml, in the most basic way.
Well even logging in more than 2 times (the maximum number I allowed) with bad credentials, I got no other error than a simple "invalid credentials". And I was able to log in quite normally afterwards.
I took the trouble to post the project on github: https://github.com/bastien70/symfony-login-throttler-test
On my main project, this is the new authenticator that I put in place and which brings me exactly the same result.</s>
I will provide more details to my answer by editing it with new elements later, the time to test everything you have offered me!
EDIT : OKAY ! I came to understand some things.
If we create a new project, and generate the authenticator directly with`
make:auth`
, it is based on the old version of the authenticator.
If we create a project, and first modify the security.yaml to tell it to use the new way to authenticate (with enable_authenticator_manager: true
), then generate an authenticator with the`
make:auth`
command, so this time it generates the new version of the authenticator (and indeed there are differences with the file that I had tried to create on my own project, in particular at the level of the Passport object in the authenticate ()).
And the <b>login_throttler</b> seems to work on it!
Here is <u>a working github repository:</u> https://github.com/bastien70/symfony-working-login-throttler
EDIT2 : I just added an issue about this new authenticator system about @Security() and @IsGranted() annotations : https://github.com/symfony/symfony/issues/40571
Hey Kiuega!
Wow! Awesome job there! Yes, you're 1000% right about make:auth... and I'm so happy that you got the login throttler working! And what a *beautiful* issue you made on Symfony - it was able to quickly look into your reproducer and understand what was going on! Though... I still can't quite spot what's wrong with @Security from looking at its code in the bundle. Anyways, use @IsGranted()... it's better anyways (but we should fix this).
Cheers!
Hello here !
Thank you for your participation in the Github issue!
You said : <blockquote>In SensioFrameworkExtraBundle, I think we should deprecate @Security.
I created @IsGranted
to be easier to use... but also because I thought the expression is not a good place to have that logic.</blockquote>
Indeed it would be better to try to solve the problem instead. According to the documentation ( https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/security.html ), the @Security
is still very useful for expressions (but if I understand your answer correctly, you would prefer that the expressions be finally moved to custom voters?), or for accessing the request and checking something (in my case I created a voter which verifies that the request was sent with Ajax, and using the @Security annotation allows me to directly access the request without bothering me, as explained in the doc https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/security.html#security ).
In the meantime, if we want to verify that a user does <b>NOT</b> have a certain role, how could we do this since we won't be able (until it's fixed I imagine), use :
@Security("not is_granted('MY_SUPER_ROLE')")
And the @IsGranted()
annotation does not allow to check a negation (unless it does, but in any case I do not see it in the documentation)
Hey Kiuega!
> but if I understand your answer correctly, you would prefer that the expressions be finally moved to custom voters
Yep! That's my general opinion... but I admit that using some basic expressions is probably ok :).
> In the meantime, if we want to verify that a user does NOT have a certain role, how could we do this since we won't be able (until it's fixed I imagine), use :
> @Security("not is_granted('MY_SUPER_ROLE')")
Hmm, I have never done this before! You want to allow access only if they do NOT have a role? Can you explain this a bit further? This sounds a bit odd... bit you might have a legitimate use-case. Anyways, for now, I would just do that logic in my controller in PHP. Or, I might create a voter - especially if it helps my code to read my clearly e..g. @IsGranted('NOT_AN_ADMIN')
Cheers!
Hello !
<blockquote>Can you explain this a bit further?</blockquote>
Yes, quite simply, I have a user who has the <b>ROLE_SUPER_ADMIN</b> role (he's the supreme boss actually), and I only want him to be able to access his sublime admin dashboard, and if he tries to surrender on a page reserved for simple users (those who have the <b>ROLE_USER</b>), then I berate him. This is why I was using the not is_granted ('ROLE_SUPER_ADMIN')
on some routes
And last question in passing (I hope you don't get bored). Since this new authentication system, the way to treat anonymous users has therefore changed. I saw that there was thus the '<b>PUBLIC_ACCESS</b>', but that seems a little vague to me.
Does making $this->isGranted ("PUBLIC_ACCESS")
make sense?
What about the "role" '<b>IS_ANONYMOUS</b>'? Could we still use it?
I find it difficult to know what we can do now to limit access to certain categories of people. For example if I have a custom voter in which I have code that allows me to know if I allow access to someone, according to several conditions:
In this case, how to manage this second part? '<b>PUBLIC_ACCESS</b>', '<b>IS_ANONYMOUS</b>', <u>or pass the token as a parameter and check that it is null</u>, which would mean that it is a guest?
All this gives me lots of doubts about everything I have to change in my code
Hey Kiuega!
> if he tries to surrender on a page reserved for simple users (those who have the ROLE_USER), then I berate him
LOL! Ok, I've never had this use-case, but it makes sense ;)
> Does making $this->isGranted ("PUBLIC_ACCESS") make sense?
> What about the "role" 'IS_ANONYMOUS'? Could we still use it?
Using PUBLIC_ACCESS in that way does not make sense, in the same way that $this->isGranted ("IS_ANONYMOUS") didn't make sense either. Both are only useful (iirc) in access_control, where you add an access_control with PUBLIC_ACCESS to *allow* access to certain URLs (and prevent the rest of the access_control from being parsed, as only the first matched access control wins).
> or pass the token as a parameter and check that it is null, which would mean that it is a guest?
this one :). As I mentioned, those other 2 attributes were only ever useful on access_control. Everywhere else in the system, just check to see if the is a user or not. Oh, and a small detail, iirc, you will be passed a NullToken to your voter (not actually null) if the user is anonymous.
Cheers!
Hello !
Okay great! Thank you very much for your time weaverryan , you have allowed me to learn a lot about this new system! I also hope that this discussion can be of use to others who will drop by in the comments section.
In the end, I created an ExtraVoter in which I will insert reusable general voters. I put the 'is_not_authenticated' there which checks that the token is a NullToken and returns true
Thanks for everything and see you next time! :)
Hey Kiuega!
> I also hope that this discussion can be of use to others who will drop by in the comments section.
Me too! It's one of the reasons I love keeping these conversations in the comments. We index them for the our search engine too... I search for old comments all the time ;).
> Thanks for everything and see you next time! :)
Cheers!
Hey again weaverryan!
After talking with the author, he's aware that configuring a custom rate limiter for login throttling is a bit tricky. He's aware of the issue. For now, he's documenting it - his documentation is similar to what I was telling you, but a bit more complete. Here's the rough draft of it - https://github.com/symfony/...
Let me know if that helps :).
Cheers!
But how Symfony knows which is Authenticator need to send credentials? We can several Authenticators specify in security.yaml
Becuase on login methods i see only
` $error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();`
How it call the Authenticator service? Thanks.
Hey triemli!
Excellent question! On each request, Symfony loops over all of the authenticators that you have defined and calls the supports()
method on ALL of them. Usually, either zero or one authenticator will return true. For example, if the user is sending a POST request to /login, then our LoginFormAuthenticator will return "true" from supports.
Once Symfony finds an authenticator whose supports method returns true, it then calls getCredentials()
, getUser()
and checkCredentials
on that authenticator. If any of those fail, then it calls onAuthenticationFailure
on that one authenticator.
On this situation, our LoginFormAuthenticator
does TWO important things during that process:
1) In getCredentials()
we call $request->getSession()->set(Security::LAST_USERNAME, $credentials['email']);
. In your login method, the $authenticationUtils->getLastUsername();
is actually a shortcut to READ this key from the session. That is why that logic works inside your login controller method. There's no magic: it's simply that our login authenticator stores the username that was last used in the session, then we read it in the controller.
2) In the parent class' onAuthenticationFailure(), the error is also stored in the session - https://github.com/symfony/symfony/blob/6d521d40721104ac684565fe6d1ca2bb0372127e/src/Symfony/Component/Security/Guard/Authenticator/AbstractFormLoginAuthenticator.php#L42. When you call $authenticationUtils->getLastAuthenticationError();
, that is just a shortcut to read that from the session.
I hope that clears things up - but please let me know if some parts are still confusing :).
Cheers!
Hi, I have some issue here.
I made /api-login annotation in the SecurityController but I see "too many redirects" an error.
I fixed with
firewalls:
login_firewall:
pattern: ^/api-login
anonymous: ~
in security yaml, but actually doesn't have an idea what'w wrong was here ;[
Hi triemli!
I think I can explain that :). See the text/video right around this code block: https://symfonycasts.com/screencast/symfony-security/firewalls-authenticator#codeblock-e09d08a770
If you don't have anonymous: true, then it means "don't allow anyone - even anonymous users to access this firewall". I normally recommend that you always have this setting (and then you can restrict access in other ways, like access_control.
But... this may only be part of the answer. I noticed that you created an entirely new firewall for this. Typically (but not always) an application only needs one firewall (if you don't count the sort of, fake, firewall that just prevents you from making the web debug toolbar and profiler not accessible - https://github.com/symfony/recipes/blob/62271a461b640d2d91fd8aaa779f6d65d7a66ec0/symfony/security-bundle/5.1/config/packages/security.yaml#L6-L8 ). On each request, only a single firewall matches - and it checks from top to bottom. So, I DO think that your problem is that you were missing the anonymous: true
on your firewall. However, I would normally expect you to have only one real firewall and for that one real firewall to have this key - similar to what the default firewall configuration looks like - https://github.com/symfony/recipes/blob/62271a461b640d2d91fd8aaa779f6d65d7a66ec0/symfony/security-bundle/5.1/config/packages/security.yaml#L9-L12
Let me know if that helps... or makes any sense. If you have more questions, let me know ;).
Cheers!
Thanks a lot!
That was a problem. I didn't notice that another firewall was created. Before I couldn't open the login page without those lines.
I also tried to say in access_control { path: ^/api-login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
On example anonymous is <b>true</b> but it works even when i set anonymous: false
(idk why xD):
Now I just added
`access_control:
Basically, I just wanted to change /login on /api-login. Because it will 2 different auth services. So I couldn't open /api-login
I even didn't reach the <b>support</b> method
Hey triemli
So, is it working now with the code you linked? It looks OK to me, it's basically the default config, you just need to start restricting some paths in the access_control
section
Thanks! Yes, everything is work good.
But 1 thing: - { path: ^/api-login, roles: ROLE_ADMIN }
access_control rules works for everything except of /api-login page. Even I specify anonymous: false
it still works and open but only /api-login page.
You may have another rule that's overruling it. The access control list reads from top to bottom and the first one always wins. { path: ^/api-login, roles: ROLE_ADMIN }
means that any URL that starts with "/api-login" it will enforce the user to have the ROLE_ADMIN but it may be happening what I just said. Double-check your other rules
Yes, but I have only 1 rule. Also access_control can't be rewritten. Actually I was expected to see access denied but I perfectly open /api-login with no problems as anonym.
Hmm, this is strange! So, if I understand things correctly, you have just ONE access_control, which is:
- { path: ^/api-login, roles: ROLE_ADMIN }
And you ARE able to access /api-login
but you are NOT able to access /api-login/anything-else
(this requires ROLE_ADMIN). Is that correct?
If so, that's very odd behavior indeed! That access control should match EVERY url starting with /api-login
including the exact URL /api-login
. Could you post your full security.yaml
by chance?
Cheers!
Before probably was aggressive cache. I manually removed var folder
This is full security.yaml
https://pastebin.com/CRQK7u6e
closed: /api-login
closed: /api-login?username=value@val.com
open: /api-login?username=value@val.com&password=pa$$w0rd //I see JSON answer "true"
Hey triemli!
Ok, I think I know what's going on - it's a "flow" that I had not thought about before :). Here is what (I think) happens:
1) When you access /api-login (via any of the examples you gave above), then the one access_control IS activated. At that moment, you are not authenticated yet (even if you send valid authentication, Symfony hasn't checked it yet - it's "lazy" about doing that). But, before denying you access, it tries to see if you *should* be authenticated by calling your authenticator.
2) In the 3rd case above, your authentication IS successful and you are now authenticated.
3) Finally, the access_control (now that it has at least attempted the authentication system) enforces the ROLE_ADMIN. In the first 2 cases above, you are still anonymous, so you are kicked out. In the 3rd case (I'm assuming the value@val.com user has ROLE_ADMIN) you are allowed access.
So, I think it makes sense :). The "normal" rule is that your login page MUST be accessible anonymously (and this is what I was thinking too), but that is NOT the case in an API situation where you can directly attach your credentials to that very same request. Your login page only needs to be anonymously accessible in a traditional form login where an anonymous user literally needs to be able to "load" /login anonymously before submitting the form.
tl;dr this makes sense - but I was thinking that it didn't. My mistake! Keep up the good work :).
Cheers!
Thanks for the answer! Yeah actually make sense then, but a small thing here: this is user *value@val.com* has a ROLE_USER role, he's not an admin ;] In this case then I would expect auth process and deny.
Hey triemli!
Hmm. Ok, yea that (I think) still makes sense. The full process looks like this:
A) The access_control matches. In order to see if the user has access, the security system is started
B) The security system successfully authenticates the user
C) The security system determines the "authentication success JSON" to send back
D) That is returned... and we're done! The original access_control stuff never has a chance to be enforced
I'm only 98% sure this is what's happening... but it makes sense. The /api-login page is never *really* accessed - it starts to be accessed, but is intercepted by the security system and never continues. But, I admit, it's not a flow I had thought of before - I would normally not cover the "login" URL with an access_control (but it is interesting!).
Cheers!
Hi,
When I create my CustomAuthenticator following your example, I get this error-message:
<blockquote>The "App\Security\CustomAuthenticator::getUser()" method must return a UserInterface. You returned "App\Entity\IvyUser".</blockquote>
My getUser method looks like this:
<br />public function getUser($credentials, UserProviderInterface $userProvider)<br />{<br />return $this->ivyUserRepository->findOneBy(['id' => $credentials['id']]);<br />}<br />
Any idea what I'm doing wrong?
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
"knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
"knplabs/knp-time-bundle": "^1.8", // 1.8.0
"nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.2.0
"stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
"symfony/asset": "^4.0", // v4.1.4
"symfony/console": "^4.0", // v4.1.4
"symfony/flex": "^1.0", // v1.17.6
"symfony/framework-bundle": "^4.0", // v4.1.4
"symfony/lts": "^4@dev", // dev-master
"symfony/orm-pack": "^1.0", // v1.0.6
"symfony/security-bundle": "^4.0", // v4.1.4
"symfony/serializer-pack": "^1.0", // v1.0.1
"symfony/twig-bundle": "^4.0", // v4.1.4
"symfony/web-server-bundle": "^4.0", // v4.1.4
"symfony/yaml": "^4.0", // v4.1.4
"twig/extensions": "^1.5" // v1.5.2
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
"easycorp/easy-log-handler": "^1.0.2", // v1.0.7
"fzaninotto/faker": "^1.7", // v1.8.0
"symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
"symfony/dotenv": "^4.0", // v4.1.4
"symfony/maker-bundle": "^1.0", // v1.7.0
"symfony/monolog-bundle": "^3.0", // v3.3.0
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
"symfony/profiler-pack": "^1.0", // v1.0.3
"symfony/var-dumper": "^3.3|^4.0" // v4.1.4
}
}
Heyi I am trying to add some validation to login form.
I have followings:
class LoginType extends AbstractType
{
}
Cannot check if an unsubmitted form is valid. Call Form::isSubmitted() before Form::isValid().
`
How can we achieve the validation and the validation errors in a form authenticator?