Login Form Authenticator

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Now 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.

The supports() Method

The 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 %}

The Brand-New dd() Function

Oh, 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?

The getCredentials() Method

Back 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().

The getUser() Method

Let'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!

The checkCredentials() Method

Ok, 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.

Leave a comment!

  • 2020-05-04 weaverryan

    Oh..... I see. That darn word "Authenticator" can mean so many things :). 2fa is not something that works easily in Symfony yet - I hope that will change soon, there are many people thinking about it (including myself). It's totally possible, you just need to build a few of the pieces yourself. Here is a conversation talking about it: https://symfonycasts.com/sc...

    I hope that helps!

    Cheers!

  • 2020-05-04 Tutorialwork

    I mean how I can implement 2 factor authentication with the Google Authenticator?

  • 2020-05-04 weaverryan

    Hey Tutorialwork!

    Check out https://github.com/knpunive... - it has a Google client and docs about creating a "social authenticator" to allow you to "login via Google".

    Cheers!

  • 2020-05-01 Tutorialwork

    And how I can impliment Google Authenticator?

  • 2020-03-05 Karin Westerbeek

    *facepalm*
    It works now, thanks!

  • 2020-03-04 Diego Aguiar

    I see it! it's a subtle error. Instead of route it's _route, starting with underscore

  • 2020-03-04 Karin Westerbeek


    public function supports(Request $request)
    {
    return $request->attributes->get('route') === 'app_login'
    && $request->isMethod('POST');

    }

  • 2020-03-04 Diego Aguiar

    Hey Karin Westerbeek

    That's weird indeed :)
    May I see your supports() method?

  • 2020-03-04 Karin Westerbeek

    I can see there's something weird going on with the time. Not sure if that's related at all though.

  • 2020-03-04 Karin Westerbeek

    Hi,

    My supports() function refuses to return true*. My code is exactly as in the tutorial so I really don't know what's going on here.
    Here's what's happening in the web server terminal:

    Mar 4 12:12:02 |INFO | REQUES Matched route "app_login". method="POST" request_uri="http://localhost:8000/login" route="app_login" route_parameters={"_controller":"App\\Controller\\SecurityController::login","_route":"app_login"}
    Mar 4 12:12:02 |DEBUG| SECURI Checking for guard authentication credentials. authenticators=1 firewall_key="main"
    Mar 4 12:12:02 |DEBUG| SECURI Checking support on guard authenticator. authenticator="App\\Security\\LoginFormAuthenticator"
    Mar 4 12:12:02 |DEBUG| SECURI Guard authenticator does not support the request.
    Mar 4 12:12:02 |INFO | SECURI Populated the TokenStorage with an anonymous Token.
    Mar 4 13:12:02 |INFO | SERVER POST (200) /login host="127.0.0.1:8004" scheme="https"
    Mar 4 12:12:02 |INFO | REQUES Matched route "_wdt". method="GET" request_uri="http://localhost:8000/_wdt/991cca" route="_wdt" route_parameters={"_controller":"web_profiler.controller.profiler::toolbarAction","_route":"_wdt","token":"991cca"}
    Mar 4 13:12:03 |INFO | SERVER GET (200) /_wdt/991cca

    As you can see the method is 'POST' and the matched route is 'app_login', both as it's supposed to be, and still it does not return true.

    Your help will be greatly appreciated.

    Cheers, Karin

    * of course it does return true if I literally tell it to:

    public function supports(Request $request)
    {
    return 'TRUE';
    }

  • 2020-01-06 Mike

    > Ok, that's probably more explanation than you wanted, but hopefully it's helpful :)

    You are awesome!
    Your answer is like a superb private coaching, thank you!

  • 2020-01-06 weaverryan

    Hey Mike!

    Interesting question :).

    > One question, is a new UserChecker the right way of doing a additional user checks (i.ex. "brute force prevention")?

    If you're trying to prevent someone from "brute forcing" your login form to "try a lot of passwords", then the user checker might be the perfect place... or not ;). There are 2 things that you might want to prevent:

    A) You might want to prevent a certain number of failed login attempts to a specific user account. I think this is what you are trying to do and I think the UserChecker is a nice place for this. But not the only place (I'll expand on that below).

    B) You might want to prevent a "bot" from testing user and passwords on your site in general. And if they try a bunch for a specific user, you might want to block that bot but *not* lock out the user's account. For this, the user checker is not the best place because you really want to block these users as early as possible - even before you query for the user object. We actually block these by using CloudFlare - you can actually only make 5 POST requests to /login on SymfonyCasts before being blocked for 15 minutes.

    Now that we've covered that, let's talk more about (A), because I think that's what you're trying to accomplish. Using checkPreAuth in the UserChecker is a pretty good solution. There is only one downside to be aware of: the user checker is checked on EVERY authentication mechanism. This actually may be *exactly* what you want... or it may not make any difference for you... I just want to be sure ;). Let me give you an example: suppose someone tries to login 10 times and you lock out their user account (I'm guessing you will use some flag/counter on the User object to keep track of this). Suppose also that you allow people to log in via GitHub or Facebook and this user has already (in the past) logged in via Facebook - so their User record is already "tied" to their Facebook account in your system. So, the user decides: "Ok, I can't remember my password right now, so I'll log in via my Facebook account". They click to do this, log into Facebook, and are redirected back to your site. Should they be logged in? Or should they be blocked?

    If you use the user provider mechanism, the user would be blocked in this situation: they cannot log in via *any* mechanism until your user checker starts "allowing" it again. This might be perfect, or might not be what you want. If it is NOT what you want, then I would put the logic directly into your authentication system. In practice, this works easily only if you're using Guard authentication, where you can easily put this same logic into the checkPassword method, for example.

    Ok, that's probably more explanation than you wanted, but hopefully it's helpful :). About your last question:

    > Should the business logic (20 lines of code) of the brute force check (max 10 login attempts in 10 minutes) be placed inside the checkPreAuth() method or inside a "new Service" which then gets called inside the checkPreAuth() method?

    Both are fine. If the logic is 10 fairly simple lines of code, you *can* move this into its own service class, but it doesn't feel necessary. I'm mostly motivated to create a new service class for logic if either (A) I want to re-use some logic or (B) if some logic is getting so complex that it's making my (in this case) use provider difficult to read.

    Cheers!

  • 2020-01-05 Mike

    Thank you for that awesome tutorial!

    One question, is a new UserChecker the right way of doing a additional user checks (i.ex. "brute force prevention")?


    Added new UserChecker:
    class UserChecker implements UserCheckerInterface
    {
    ...
    public function checkPreAuth(UserInterface $user)
    {
    ...
    // 20 lines of code for custom logic

    1.) Is a new UserChecker the right place?

    2.) Should the business logic (20 lines of code) of the brute force check (max 10 login attempts in 10 minutes) be placed inside the checkPreAuth() method or inside a "new Service" which then gets called inside the checkPreAuth() method?

    Iam not yet clear about when to create a new service and when to write the business logic inside a method itself.

    Thanks in advance for your time!

  • 2019-10-01 brentmtc

    Exactly what I needed to get it to work. Thank you!

  • 2019-09-24 Diego Aguiar

    Hey brentmtc

    You may want to watch this chapter where Ryan explains how to do exactly that :)
    https://symfonycasts.com/sc...

    Cheers!

  • 2019-09-24 brentmtc

    Hello,
    In my LoginFormAuthenticator I am overriding the onAuthenticationFailure method to check an additional last_login field on the user table. If greater than 60 days I am throwing an AccountExpiredException. I am having trouble customizing the message that gets displayed. So far, I have only got the default message to display.

    Any ideas on how I can customized the message? Thank you in advance.

    onAuthenticationFailure method in my LoginFormAuthenticator:



    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)

    {

    if ($request->hasSession()) {

    $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);

    }


    if($exception instanceof AccountExpiredException) {

    $url = $this->urlGenerator->generate('app_account_expired', array('email' => $exception->getUser()->getEmail()));

    } else
    {
 $url = $this->getLoginUrl();


    }

 return new RedirectResponse($url);

    }

    my Userchecker.php



    namespace App\Security;



    use App\Entity\User\User,

    Symfony\Component\Security\Core\Exception\AccountExpiredException,

    Symfony\Component\Security\Core\User\UserCheckerInterface,

    Symfony\Component\Security\Core\User\UserInterface;





    class UserChecker implements UserCheckerInterface
    
 {



    public function checkPreAuth(UserInterface $user)
 {

    if (!$user instanceof User) {

    return;

    }



    }



    public function checkPostAuth(UserInterface $user)
 {



    // check if last_login is greater than 60 days

    if($user->getLastLogin()->diff(new \DateTime('now'))->days > 60) {


    $ex = new AccountExpiredException('Your account has expired. Please change your password to login again.');

    $ex->setUser($user);

    throw $ex;



    }


    }


    }

  • 2019-03-12 Victor Bocharsky

    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!

  • 2019-03-11 Mike

    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?)

  • 2019-02-13 Diego Aguiar

    Awesomeness!

  • 2019-02-12 Rob Steuber

    Tnxs Diego, Problem solved!

    Cheers 🍻

  • 2019-02-11 Diego Aguiar

    Hey Rob Steuber

    Check at your "User::getRoles()" method. I believe you are returning null instead of an array

    Cheers!

  • 2019-02-11 Rob Steuber

    Hello, I get this error:

    Argument 3 passed to Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken::__construct() must be of the type array, null given, called in /var/www/vendor/symfony/security-guard/AbstractGuardAuthenticator.php on line 38

    Anybody knows what is going on, cause i got no idea.

    Thanks in advance!
    Cheers Rob

  • 2018-10-31 Camille Seuvin

    Thank you for your reply weaverryan !

  • 2018-10-29 weaverryan

    Hey Camille Seuvin !

    This can be a bit confusing at first :). Basically, it's just built that way. What I mean is, behind the scenes in Symfony, this is a simplified version of what happens


    // deep, dark core Symfony code
    // $authenticator is YOUR authenticator object

    if (!$authenticator->supports($request)) {
    return;
    }

    // Symfony calls your getCredentials()
    $credentials = $authenticator->getCredentials($request);
    // then immediately calls getUser() and passes your credentials right back to you
    $user = $authenticator->getUser($credentials);

    Let me know if that helps! Symfony calls these two methods on your authenticator right after each other. IT knows to pass what you returned from getCredentials() into getUser().

    Cheers!

  • 2018-10-29 weaverryan

    Hey Yahya A. Erturan!

    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!

  • 2018-10-28 Camille Seuvin

    Hello,
    How does the function getUser() know that the $credentials parameter is what is returned by the function getCredentials() ?

  • 2018-10-27 Yahya A. Erturan

    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;
    }

  • 2018-10-27 Yahya A. Erturan

    Heyi I am trying to add some validation to login form.

    I have followings:


    # LoginFormAuthenticatior.php
    public function getCredentials(Request $request)
    {
    $form = $this->formFactory->create(LoginType::class);

    if($form->isValid())
    {
    $form->handleRequest($request);
    $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;
    }
    else
    {
    return $form->getErrors();
    }
    }


    class LoginType extends AbstractType
    {
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
    $builder
    ->add('email', TextType::class, [
    'required' => true,
    'constraints' => array(new Email())
    ])
    ->add('password', PasswordType::class)
    ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
    $resolver->setDefaults(array(
    // enable/disable CSRF protection for this form
    'csrf_protection' => true,
    // the name of the hidden HTML field that stores the token
    'csrf_field_name' => '_csrf_token',
    // an arbitrary string used to generate the value of the token
    // using a different string for each form improves its security
    'csrf_token_id' => 'authenticate',
    ));
    }
    }

    But when I click submit in login form;

    This throws the following error:

    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?

  • 2018-10-18 Victor Bocharsky

    Hey Serge,

    It's just a simple if, so no performance hit I think :) You have hundred or thousand (or even more?) if statements in your project. Anyway, any feature do some performance impact, but that does not mean you should not use it.

    Cheers!

  • 2018-10-18 Serge Boyko

    Isn't it a performance hit to check "supports" on every request?

  • 2018-10-15 Diego Aguiar

    Ohh, that's interesting... and yeah, the error message is not very helpful in this situation. Cheers!

  • 2018-10-15 Mr Magoo

    Thanks Diego, worked it out - it wasn't to do with supports(), it was getUser(). I'm sort of roughly following the tutorial applying the lessons to a project I am working on and I messed up the parameters on the doctrine query - In my tiredness I made the mistake of querying it on a property that the entity did not have (username). So for anyone else that's the kind of message you'll get back when your query is not returning anything. Silly me.

  • 2018-10-12 Oussama EZZIOURI

    Everything is ok regarding the logging and redirection to homepage.

    I will work on translation and error handling this evening and I'll give you a feedback.

    Thank's Victor.

  • 2018-10-12 Victor Bocharsky

    Hey Oussama,

    About 1st: Where does this message is set? Do you translate this message in the template with "|trans()" Twig filter? Did you enable translator in your system? If so, you need to clear the cache (even for dev environment) when you create a new translation file, otherwise Symfony won't see it. So, first of all, try to clear the cache. Here's the link for the reference: https://symfonycasts.com/sc...

    About 2nd: Oh, you do it wrong :) You pass to response just an error message, and it's sent to the user, i.e. you sent to users just an empty page with only error message in it. Looks like you're trying to do it in advance of this screencast. Please, watch a few more videos in this course, where we're talking about handling errors, e.g.:
    https://symfonycasts.com/sc...

    So in short, please, try to watch this course completely. We have covered this questions in it, but as I see you just missed some parts, probably you watched this course selectively?

    Cheers!

  • 2018-10-12 Victor Bocharsky

    Hey Oussama,

    Nope, you need to return it instead of sending, that's the correct way how it should work. If it's not redirect you, then you have some other problems you should fix first.

    Cheers!

  • 2018-10-11 Oussama EZZIOURI

    Hi Victor,

    Thank you for your replay. I've comment the Serializable interface and 2 methods: serialize() and unserialize(), now everything work like a charm! I got the username in the profiler bar.

    Now I've 2 issues :

    The 1st issue is with translation, I've created the translations/security.en.yaml file with the following content

    "Username could not be found.": "Incorrect login and password combination !"

    But when I login with bad credentials, I still get the standard message "Username could not be found."

    The 2nd issue is when I get The "Username could not be found." message, the browser stopped in a blank page with this error message, which is normal because I return a Response in onAuthenticationFailure() method :


    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
    return new Response($exception->getMessageKey());
    }

    My question now, is how can I dispaly this error in the login form ?

    Best regards.
    Symfony Lover.

  • 2018-10-11 Diego Aguiar

    Hey @Dan

    It's there a chance that you have something wrong in your supports() method? because if it doesn't return "true", then, the authentication flow stops and the request continues its lifecycle.

    Cheers!

  • 2018-10-11 Oussama EZZIOURI

    Hello my friend,

    Thank you for taking care of my comment, I will take this in considration in my future comments.

    For the response I've tried to return the response but it doesn't redirect me, so I've add send() method.

    But i still have the same problem with anonymous session. Could you please help me to solve it.

    Le jeu. 11 oct. 2018 10:24, Disqus <notifications@disqus.net> a écrit :

  • 2018-10-11 Dan

    Hey there I seem to have hit a snag - everything works until the supports function and then at getCredentials dump doesn't work and I get a huge red error "Cannot redirect to an empty URL." Do you have any idea what that might be caused by? Thanks a bunch.

  • 2018-10-11 Victor Bocharsky

    Hey Oussama,

    Looks like your comment was marked as spam by Disqus because it's long and contain HTML tags, I just recovered it from spam folder and modified slightly - wrap code with pre/code tags - it highlights syntax and give you more chances to bypass Discus spam system, just wanted to let you know.

    What about your question, hm, in onAuthenticationSuccess() just return the response, don't need to send it manually. It will be sent later automatically in another part of the system, i.e:


    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
    return new RedirectResponse($this->router->generate('app_homepage'));
    }

    I see you use serialize()/unserialize() in your User class, that may be a problem with serialization/unserialization. Could you temporary comment out implementing \Serializable interface and those 2 methods: serialize() and unserialize()? Does it work without it? Now are you logged in in the Symfony web profiler?

    Cheers!

  • 2018-10-10 Oussama EZZIOURI

    Hi, Thank you for this great tutorial.

    I've succeed to login and redirect to my homepage, but the User still anonymous in the symfony profiler bar. This my code :


    namespace App\Security;

    use App\Repository\UserRepository;
    use Symfony\Component\HttpFoundation\RedirectResponse;
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\Routing\RouterInterface;
    use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
    use Symfony\Component\Security\Core\Exception\AuthenticationException;
    use Symfony\Component\Security\Core\User\UserInterface;
    use Symfony\Component\Security\Core\User\UserProviderInterface;
    use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;

    class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
    {
    private $userRepository;
    private $router;

    public function __construct(UserRepository $userRepository, RouterInterface $router)
    {
    $this->userRepository = $userRepository;
    $this->router = $router;
    }
    public function supports(Request $request)
    {
    return $request->attributes->get('_route') === 'app_login'
    && $request->isMethod('POST');
    }

    public function getCredentials(Request $request)
    {
    return [
    'email' => $request->request->get('email'),
    'password' => $request->request->get('password'),
    ];
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
    return $this->userRepository->findOneBy(['email' => $credentials['email']]);
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
    return true;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
    dd('failure!');
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
    $response = new RedirectResponse($this->router->generate('app_homepage'));
    $response->send();
    }

    public function start(Request $request, AuthenticationException $authException = null)
    {
    // todo
    }

    public function supportsRememberMe()
    {
    // todo
    }

    /**
    * Return the URL to the login page.
    *
    * @return string
    */
    protected function getLoginUrl()
    {
    // TODO: Implement getLoginUrl() method.
    }
    }

    And for the user entity code :


    namespace App\Entity;

    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Component\Security\Core\User\UserInterface;

    /**
    * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
    */
    class User implements UserInterface, \Serializable
    {
    /**
    * @ORM\Id()
    * @ORM\GeneratedValue()
    * @ORM\Column(type="integer")
    */
    private $id;

    /**
    * @ORM\Column(type="string", length=255)
    */
    private $fname;

    /**
    * @ORM\Column(type="string", length=255)
    */
    private $lname;

    /**
    * @ORM\Column(type="string", length=255)
    */
    private $password;

    /**
    * @ORM\Column(type="string", length=255)
    */
    private $agency;

    /**
    * @ORM\Column(type="string", length=255)
    */
    private $email;

    /**
    * @ORM\Column(type="json", nullable=true)
    */
    private $roles = [];

    /**
    * @ORM\Column(type="string", length=255)
    */
    private $username;

    public function getId(): ?int
    {
    return $this->id;
    }

    public function getFirstName(): ?string
    {
    return $this->fname;
    }

    public function setFirstName(string $firstName): self
    {
    $this->fname = $firstName;

    return $this;
    }

    public function getLastName(): ?string
    {
    return $this->lname;
    }

    public function setLastName(string $lastName): self
    {
    $this->lname = $lastName;

    return $this;
    }

    public function getPassword(): ?string
    {
    return $this->password;
    }

    public function setPassword(string $password): self
    {
    $this->password = $password;

    return $this;
    }

    public function getAgency(): ?string
    {
    return $this->agency;
    }

    public function setAgency(string $agency): self
    {
    $this->agency = $agency;

    return $this;
    }

    public function getEmail(): ?string
    {
    return $this->email;
    }

    public function setEmail(string $email): self
    {
    $this->email = $email;

    return $this;
    }

    /**
    * Returns the roles granted to the user.
    *
    *
    * public function getRoles()
    * {
    * return array('ROLE_USER');
    * }
    *

    *
    * Alternatively, the roles might be stored on a ``roles`` property,
    * and populated in any number of different ways when the user object
    * is created.
    *
    * @return (Role|string)[] The user roles
    */
    public function getRoles()
    {
    return array('ROLE_USER');
    }

    /**
    * Returns the salt that was originally used to encode the password.
    *
    * This can return null if the password was not encoded using a salt.
    *
    * @return string|null The salt
    */
    public function getSalt()
    {
    // TODO: Implement getSalt() method.
    }

    /**
    * Returns the username used to authenticate the user.
    *
    * @return string The username
    */
    public function getUsername()
    {
    return $this->username;
    }

    /**
    * Removes sensitive data from the user.
    *
    * This is important if, at any given point, sensitive information like
    * the plain-text password is stored on this object.
    */
    public function eraseCredentials()
    {
    // TODO: Implement eraseCredentials() method.
    }

    public function serialize()
    {
    return $this->serialize([
    $this->id,
    $this->fname,
    $this->lname,
    $this->email,
    $this->password,
    $this->username,
    $this->agency
    ]);
    }

    public function unserialize ($string)
    {
    list($this->id,
    $this->fname,
    $this->lname,
    $this->email,
    $this->password,
    $this->username,
    $this->agency) = unserialize($string, ['allowed_classes' => false]);
    }

    public function setRoles(?array $roles): self
    {
    $this->roles = $roles;

    return $this;
    }

    public function setUsername(string $username): self
    {
    $this->username = $username;

    return $this;
    }
    }
  • 2018-09-17 weaverryan

    Hey Peter Kosak!

    Ah, thanks dude! We're going to stay VERY busy writing more tutorials, don't worry ;).

    Great question! And... you already answered it ;). The only difference would be the error message - the user would always get a "Username not found" error message. However, even *that* can be controlled. At any time in your authenticator, you can throw a CustomUserMessageAuthenticationException (https://github.com/symfony/... and that message will be shown to the user. We separated getUser() from checkCredentials() only to make it a bit easier to get the right error message.

    > Why it is set up this way that firstly check user and then pw?

    We *need* to fetch the user first because we need it in *order* to check the password. That's the only reason :). This *does* mean that a user will be able to "discover" that an email address is valid or invalid by trying different email addresses. Some people don't like this from a security standpoint. If you care, you can change the error messages (via the CustomUserMessageAuthenticationException or via translations - we'll show that next) so that there is always the same message, no matter if the email or password failed.

    Cheers!

  • 2018-09-17 Peter Kosak

    Hi weaverryan,

    first of all congratulation You and whole KNP team (Victor & Diego I think sorry if I missed others) for working closer with Symfony and change of the name to SymfonyCasts great step. (I hope you wont be that busy with rewriting whole symfony doc as you said in emails, we need more videos :))

    My question is: What would be the difference if I would call in getUser method ->findOneBy and pass email & password?

    It would only return user when pw & email match so I wouldnt have to use checkCredential method.

    Is it because we will return different messages?
    One for user exists but PW is wrong and another for User does not exist please register?
    Why it is set up this way that firstly check user and then pw?

    Thanks & again big thumbs up!

  • 2018-09-14 weaverryan

    Hey Stéphane!

    Great question! I get this often. In short, I don't like the user provider :). It's actually not *really* meant to be used in this way. The main purpose of the user provider is 2 things:

    1) To help refresh the user at the beginning of every request
    2) Some core parts of security - like remember_me and switch_user - call its loadUserByUsername() when you use those

    Related to (2), if you use the form_login mechanism on your firewall, it also uses this method.

    Why don't I like it? Well, when you're writing your own code, you *already* know very well how to query for your User based on whatever condition you want. Telling people to use the user provider - an object they're not familiar with - can cause confusion. Of course, technically speaking, there's absolutely nothing wrong with using the user provider :).

    But, let me give you one more reason. IF we told people to use the user provider like you've done (which is a fine solution), it would work. But, for people using email, it looks confusing! "Hey! I don't have a username". Also, if your query logic becomes more complex - e.g. you have a third field on your login form called "companyId" and you need to query by "email & companyId", it's not possible with loadUserByUsername(). And, it's not clear *how* you would fix this. But, by making a query, it becomes very obvious later if you need to change that query with some extra logic.

    Phew! Does that make sense? I'm sure other people will have this question - so I wanted to give a clear answer. The same question was asked for our Symfony 3 security tutorial. Like I said, it's a good question :).

    Cheers!

  • 2018-09-14 Stéphane

    Hey @weaverryan,

    Why in getUser() method, you do not use $userProvider to retrieve user object like this :

    $user = $userProvider->loadUserByUsername($credentials['email']);

    There is no difference about dd($user);

    Thank for your explanation.