Authentication Errors

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

Go back to the login page. I wonder what happens if we fail the login... which, is only possible right now if we use a non-existent email address. Oh!

Cannot redirect to an empty URL

Filling in getLoginUrl()

Hmm: this is coming from AbstractFormLoginAuthenticator our authenticator's base class. If you dug a bit, you'd find out that, on failure, that authenticator class is calling getLoginUrl() and trying to redirect there. And, yea, that makes sense: if we fail login, the user should be redirected back to the login page. To make this actually work, all we need to do is fill in this method.

No problem: return $this->router->generate('app_login'):

... lines 1 - 13
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 55
protected function getLoginUrl()
{
return $this->router->generate('app_login');
}
}

Ok, try it again: refresh and... perfect! Hey! You can even see an error message on top:

Username could not be found.

We get that exact error because of where the authenticator fails: we failed to return a user from getUser():

... lines 1 - 13
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 39
public function getUser($credentials, UserProviderInterface $userProvider)
{
return $this->userRepository->findOneBy(['email' => $credentials['email']]);
}
... lines 44 - 59
}

In a little while, we'll learn how to customize this message because... probably saying "Email" could not be found would make more sense.

The other common place where your authenticator can fail is in the checkCredentials() method:

... lines 1 - 13
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 44
public function checkCredentials($credentials, UserInterface $user)
{
// only needed if we need to check a password - we'll do that later!
return true;
}
... lines 50 - 59
}

Try returning false here for a second:

// ...
    public function checkCredentials($credentials, UserInterface $user)
    {
        return false;
    }
// ...

Then, login with a legitimate user. Nice!

Invalid credentials.

Anyways, go change that back to true:

... lines 1 - 13
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 44
public function checkCredentials($credentials, UserInterface $user)
{
// only needed if we need to check a password - we'll do that later!
return true;
}
... lines 50 - 59
}

How Authentication Errors are Stored

What I really want to find out is: where are these errors coming from? In SecurityController, we're getting the error by calling some $authenticationUtils->getLastAuthenticationError() method:

... lines 1 - 6
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends AbstractController
{
... lines 11 - 13
public function login(AuthenticationUtils $authenticationUtils)
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
... lines 18 - 21
return $this->render('security/login.html.twig', [
... line 23
'error' => $error,
]);
}
}

We're passing that into the template and rendering its messageKey property... with some translation magic we'll talk about soon too:

... lines 1 - 10
{% block body %}
<form class="form-signin" method="post">
{% if error %}
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
... lines 16 - 29
</form>
{% endblock %}

The point is: we magically fetch the "error" from... somewhere and render it. Let's demystify that. Go back to the top of your authenticator and hold command or control to click into AbstractFormLoginAuthenticator.

In reality, when authentication fails, this onAuthenticationFailure() method is called. It's a bit technical, but when authentication fails, internally, it's because something threw an AuthenticationException, which is passed to this method. And, ah: this method stores that exception onto a special key in the session! Then, back in the controller, the lastAuthenticationError() method is just a shortcut to read that key off of the session!

So, it's simple: our authenticator stores the error in the session and then we read the error from the session in our controller and render it:

... lines 1 - 8
class SecurityController extends AbstractController
{
... lines 11 - 13
public function login(AuthenticationUtils $authenticationUtils)
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
... lines 18 - 25
}
}

The last thing onAuthenticationFailure() does is call our getLoginUrl() method and redirect there.

Filling in the Last Email

Go back to the login form and fail authentication again with a fake email. We see the error... but the email field is empty - that's not ideal. For convenience, it should pre-fill with the email I just entered.

Look at the controller again. Hmm: we are calling a getLastUsername() method and passing that into the template:

... 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>
... lines 20 - 29
</form>
{% endblock %}

Oh, but I forgot to render it! Add value= and print last_username:

... lines 1 - 10
{% block body %}
<form class="form-signin" method="post">
... lines 13 - 18
<input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus>
... lines 20 - 29
</form>
{% endblock %}

But... we're not quite done. Unlike the error message, the last user name is not automatically stored to the session. This is something that we need to do inside of our LoginFormAuthenticator. But, it's super easy. Inside getCredentials(), instead of returning, add $credentials = :

... lines 1 - 14
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 17 - 32
public function getCredentials(Request $request)
{
$credentials = [
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
];
... lines 39 - 45
}
... lines 47 - 67
}

Now, set the email onto the session with $request->getSession()->set(). Use a special key: Security - the one from the Security component - ::LAST_USERNAME and set this to $credentials['email']:

... lines 1 - 14
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 17 - 32
public function getCredentials(Request $request)
{
$credentials = [
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['email']
);
... lines 44 - 45
}
... lines 47 - 67
}

Then, at the bottom, return $credentials:

... lines 1 - 14
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 17 - 32
public function getCredentials(Request $request)
{
$credentials = [
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['email']
);
return $credentials;
}
... lines 47 - 67
}

Try it! Go back, login with that same email address and... nice! Both the error and the last email are read from the session and displayed.

Next: let's learn how to customize these error messages. And, we really need a way to logout.

Leave a comment!

  • 2020-02-25 Diego Aguiar

    It happens :)

  • 2020-02-25 wuwu

    it's ok, 'used' wrong class

  • 2020-02-25 Wuwu

    Anyone got this?
    Undefined class constant 'LAST_USERNAME'

  • 2020-02-03 Diego Aguiar

    Hey Mike

    Good question! And there is a better way to do it. What you need to do is to implement your own AuthenticationFailureHandler which should implement the interface Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface. So, whenever a request fails, the method onAuthenticationFailure() will be executed. You can read about security events here: https://symfony.com/doc/cur...

    I hope it helps. Cheers!

  • 2020-02-01 Mike

    Goal:
    I want to submit comments to my article via Ajax.
    At the moment, depending on the http status code, $.ajax know if the submit was successful or not.

    $.ajax({
    type: 'POST',
    url: $link,
    data: { …},
    success: function(data, status) {

    },
    error: function (result) {

    }

    Only user can submit comments, so I chooses the * @IsGranted("ROLE_USER") annotation for the createNewComment() method in my controller.

    Problem:
    If the user is not logged in, I get a 200 Status code as result of the Ajax request (with the content of the /login page).
    JS thinks the submit was successful and goes into the success: function(data, status), when it should instead go into the error: function(result).

    Possible solutions:
    1.) Return another status code (405?) on the getLoginUrl() method? (As far as I know, the getLoginUrl() action is called if the user check via isGranted fails)
    But I haven't found a way to add a status code to: return $this->urlGenerator->generate('app_login');

    2.) "Manually" check if it is a user in my createNewComment() action. But this seems to be "over engineered/not suitable", because I already have the isGranted annotation which should check exactly that.

    Question:
    Whats the right way, in an Ajax submit, to tell JS that the request was unsuccessful due to @isGranted("ROLE_USER") failed?

    UPDATE://
    I've found a great way, my solution is to override the start() method of LoginFormAuthenticator:

     
    /**
    * Override to control what happens when the user hits a secure page
    * but isn't logged in yet.
    *
    * @return JsonResponse|RedirectResponse
    */
    public function start(Request $request, AuthenticationException $authException = null)
    {
    if ( $request -> isXmlHttpRequest() ) {
    $data = [
    'message' => 'Authentication Required'
    ];

    return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
    } else {
    $url = $this->getLoginUrl();

    return new RedirectResponse($url);
    }
    }


    It works this way.
    Is that the right way of doing it?

  • 2019-10-07 Victor Bocharsky

    Hey Dung,

    Yes, that's our main goal, make short and clear tutorials about complex topics. Glad you like it ;)

    Cheers!

  • 2019-10-05 Dung Le

    Thanks for all the replies! Victor Bocharsky . BTW, the tutorials made into a few minutes long sections makes it easy to learn/digest the materials, great idea!

  • 2019-10-04 Victor Bocharsky

    Hey Dung,

    Haha, that's a good plan, we definitely should think about it til the next September when the new school round begin :p ;)

    About "<3" - this has a super very important meaning, but also it's a HUGE secret, so don't tell anyone, please! This means "heart", like <3 is equal to ❤ but is written in old-school chars... this was even before any Emoji, etc. So, I hope it makes sense to you ;)

    Cheers!

  • 2019-10-03 Dung Le

    Thanks Victor Bocharsky, Symfonnycast is awesome (I wish public/school libraries own a copy :). I found the README.md, I will follow that to setup a practice project. BTW, in the READ has a text "<3 Your friends at KnpUniversity" what is the meaning of "<3"?

  • 2019-10-03 Victor Bocharsky

    Hey Dung,

    You don't have to follow the whole course. We described what you have to do to bootstrap the project locally in README.md file in downloaded archive. Basically, it says you need to look into start/README.md or finish/README.md, depends on what you need - start or finish project code. There will be some instructions you need to execute to create the DB and its schema - Symfony has a few commands.

    So, basically, look into README files inside the archive :)

    Cheers!

  • 2019-10-03 Dung Le

    Hello all,

    I do not follow tutorial from beginning to the end, the way I use these tutorials is just read chapter(s) I am interested in, so when I want to test the code provided in the downloads, where can I find the database? or do I have to follow all chapters in order to generate the database as I go along with the tutorial?

    Thanks!

  • 2019-04-05 Ronald

    I got this too and looking a solution for this problem until i checked the comments. :)

  • 2019-03-27 Victor Bocharsky

    Hey Diego,

    Wait, but "Security::LAST_USERNAME" is just a sting, i.e. just a key that Symfony will use internally to fetch the last entered username, i.e. email in our case. So, we use it instead of hardcoding the specific "_security.last_username" key name manually.

    Cheers!

  • 2019-03-26 Diego

    Hi, when you store the username in the session, why did you use "Security::LAST_USERNAME" instead of "$request->request->get('email')" as a few lines above?

    Thanks!

  • 2019-02-05 bahae

    very grateful! Thank you!

  • 2019-02-05 weaverryan

    Hey bahae!

    Nice work! Yes, the AbstractFormLoginAuthenticator class that your authenticator extends implements the onAuthenticationFailure() method and IT is responsible for taking the error and putting it onto the session so that it's accessible via the error.messageKey. So, your solution was perfect - you were overriding this functionality on accident - but you definitely want it :).

    Cheers!

  • 2019-02-02 bahae

    Hi again friends,
    i find the solution of this dummy problem, when i generated my LoginFormAuthenticator, the console generate lot of methods like [supports(),getCredentials(),...] one of them named (onAuthenticationFailure()) this is the problem !! delete it and everything works great.
    Cheers! XD

  • 2019-02-02 bahae

    Hi friends,
    first of all I want to say thanks for this great and very helpful work!!
    My problem is i can't find the error message when i try to login with a false email. even if my template have "error.messageKey", and my controller have also rendered the "error" key!
    any solution for that?

  • 2019-01-14 weaverryan

    Hey OutspOaken!

    Hmm, that's a bit misleading. If you've configured your app to have a session (which is the default in a new application thanks to this line in the recipe: https://github.com/symfony/... then you don't need to worry about this. What changed is that, until now, if you did NOT have a session configured, calling $request->getSession() was allowed, but it would return null. In Symfony 5, instead of returning null, it will throw an exception.

    So, if you know that your app uses a session, you're fine to just se things directly on the session. But if you were building some re-usable bundle, you would want to check first that the session exists.

    Cheers!

  • 2019-01-14 OutspOaken

    It seems we have to check if a session exists before getting it since Symfony 4.1:
    https://symfony.com/blog/ne...

    So, to set the email onto the session, we have to do:
    if ($request->hasSession() && ($session = $request->getSession())) {
    $session->set(Security::LAST_USERNAME, $credentials['email']);
    }

    Is that correct ?

  • 2018-09-19 Axel Verdruye

    thanks! No its fine :) I'll rewatch the videos. Always good to refresh the memory. ty for the response. Great tutorials btw !

  • 2018-09-17 Victor Bocharsky

    Hey Axel,

    We count progress on video watching, so it's impossible to get a certificate for an unfinished course, since since it does not have all the videos. Moreover, the chapters that are not released yet is just a draft, that can be changed. But yeah, most of the time it does not change or we do very minor changes in it. So, if you won't want to watch the new videos when they are released - ping us again when this course is completely released and we'll give you the certificate.

    Cheers!

  • 2018-09-17 Axel Verdruye

    Hi, im completing the course without the videos (as they are not yet implemented). But how do i complete the certificate? As the course % depends on the amount of videos you viewed.

    Thanks !