Buy

Logging out & Pre-filling the Email on Failure

Check this out: let's fail authentication with a bad password.

Ok: I noticed two things. First, we have an error:

Invalid credentials.

Great! But second, the form is not pre-filled with the email address I just used. Hmm.

Behind the scenes, the authenticator communicates to your SecurityController by storing things in the session. That's what the security.authentication_utils helps us with:

36 lines src/AppBundle/Controller/SecurityController.php
... lines 1 - 8
class SecurityController extends Controller
{
... lines 11 - 13
public function loginAction()
{
$authenticationUtils = $this->get('security.authentication_utils');
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
... lines 20 - 34
}
}

Hold command and open getLastAuthenticationError(). Ultimately, this reads a Security::AUTHENTICATION_ERROR string key from the session.

And the same is true for fetching the last username, or email in our case: it reads a key from the session.

Here's the deal: the login form is automatically setting the authentication error to the session for us. But, it is not setting the last username on the session... because it doesn't really know where to look for it.

No worries, fix this with $request->getSession()->set() and pass it the constant - Security::LAST_USERNAME - and $data['_username']:

77 lines src/AppBundle/Security/LoginFormAuthenticator.php
... lines 1 - 9
use Symfony\Component\Security\Core\Security;
... lines 11 - 14
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 17 - 27
public function getCredentials(Request $request)
{
... lines 30 - 38
$data = $form->getData();
$request->getSession()->set(
Security::LAST_USERNAME,
$data['_username']
);
... lines 44 - 45
}
... lines 47 - 75
}

Now, try it again. Good-to-go!

Can I Logout?

Next challenge! Can we logout? Um... right now? Nope! But that seems important! So, let's do it.

Start like normal: In SecurityController, create a logoutAction, set its route to /logout and call the route security_logout:

44 lines src/AppBundle/Controller/SecurityController.php
... lines 1 - 8
class SecurityController extends Controller
{
... lines 11 - 36
/**
* @Route("/logout", name="security_logout")
*/
public function logoutAction()
{
... line 42
}
}

Now, here's the fun part. Don't put any code in the method. In fact, throw a new \Exception that says, "this should not be reached":

44 lines src/AppBundle/Controller/SecurityController.php
... lines 1 - 8
class SecurityController extends Controller
{
... lines 11 - 39
public function logoutAction()
{
throw new \Exception('this should not be reached!');
}
}

Adding the logout Key

Whaaaat? Yep, our controller will do nothing. Instead, Symfony will intercept any requests to /logout and take care of everything for us. To activate it, open security.yml and add a new key under your firewall: logout. Below that, add path: /logout:

31 lines app/config/security.yml
... lines 1 - 2
security:
... lines 4 - 9
firewalls:
... lines 11 - 15
main:
... lines 17 - 21
logout:
path: /logout
... lines 24 - 31

Now, if the user goes to /logout, Symfony will automatically take care of logging them out. That's super magical, almost creepy - but it works pretty darn well.

So, why did I make you create a route and controller if Symfony wasn't going to use it? Am I trying to drive you crazy!

Come on, I'm looking out for you! It turns out, if you don't have a route that matches /logout, then the 404 page is triggered before Symfony has a chance to log the user out. That's why you need this.

It should work already, but let's add a friendly logout link. In base.html.twig, how can we figure out if the user is logged in? We're about to talk about that... but what the heck - let's get a preview. Use {% if is_granted('ROLE_USER') %}:

53 lines app/Resources/views/base.html.twig
<!DOCTYPE html>
<html>
... lines 3 - 13
<body>
... lines 15 - 19
<header class="header">
... lines 21 - 22
<ul class="navi">
... line 24
{% if is_granted('ROLE_USER') %}
... lines 26 - 27
<li><a href="{{ path('security_login') }}">Login</a></li>
{% endif %}
</ul>
</header>
... lines 32 - 50
</body>
</html>

Remember this role? We returned it from getRoles() in User - so all authenticated users have this.

If they don't have this, show the login link. But if they do, show the logout link: path('security_logout'):

53 lines app/Resources/views/base.html.twig
<!DOCTYPE html>
<html>
... lines 3 - 13
<body>
... lines 15 - 19
<header class="header">
... lines 21 - 22
<ul class="navi">
... line 24
{% if is_granted('ROLE_USER') %}
<li><a href="{{ path('security_logout') }}">Logout</a></li>
{% else %}
<li><a href="{{ path('security_login') }}">Login</a></li>
{% endif %}
</ul>
</header>
... lines 32 - 50
</body>
</html>

Perfect!

Try the whole thing out: head to the homepage. We're anonymous right now.. so let's login! Cool! And there's the logout link. Click it! Ok, back to anonymous. If you need to control what happens after logging out, check the official docs on the logout stuff.

Alright. Now, as much as I like turtles, we should probably give our users a real password.

Leave a comment!

  • 2017-08-04 weaverryan

    Haha! Yes! So happy it worked - that was a fun challenge :). Thanks for posting your solution here, in full beautifully formatted code too.

    Cheers!

  • 2017-08-04 Patrick Vale

    Hi weaverryan,

    Thanks for the information - that's really helpful!

    Looking at the logout handling code, it did seem like maybe I needed just half of what 'invalidate_session' does - just the migration of the session to a new id, rather than doing that and clearing it out as well.

    I created a simple logout handler


    class LogoutHandler implements LogoutHandlerInterface
    {
    public function logout(Request $request)
    {
    $session = $request->getSession();
    $session->migrate(true);
    }

    }

    which is registered in security.yml


    public_users:
    anonymous: ~
    logout:
    path: /logout
    invalidate_session: false
    handlers: [app.logout_handler]
    admin_users:
    pattern: ^/admin
    anonymous: ~
    logout:
    path: /admin/logout
    target: /admin/login
    invalidate_session: false
    handlers: [app.logout_handler]

    And now, when I attempt a logout, irrespective of whether the page is still loading or not, it logs me out. And I still get to log out of one firewall, but not the other, which is awesome!

    Thanks again!
    Patrick

  • 2017-08-03 weaverryan

    Hey Patrick Vale!

    Hmm. I don't have a 100% answer for you, but I can share some info at least. When you add invalid_session, it causes only 1 change in the system: when you logout, this line is called: https://github.com/symfony/...

    Regenerating the session clears all the old data, but it also gives the user a completely new session id, which should mean that even if you *did* have some other request running in the background, that request would try to store data in the *old* session, which would not affect the new session. But check out the invalidate method: https://github.com/symfony/...

    It *clears* the storage, and then uses migrate() to create the new session. If you're reading the situation correctly about race conditions (makes sense to me), then the fix might be to NOT call invalidate(), but instead to call $session->migrate(true) (the true destroys the old session - which is just safer). So, I would try removing the invalidate_session config key, but instead, adding your own logout handler that is really similar to the SessionLogoutHandler, but which calls migrate(true) instead of invalidate().

    Let me know what you find out! You always have interesting questions :)

    Cheers!

  • 2017-08-03 Patrick Vale

    Having looked into it further, it seems that the behaviour where a request to '/logout' does not log the user out, only occurs if you click the '/logout' while the original page is still loading (in this case, due to a large page with many image assets loading using app_dev.php).

    If I wait until the page load is complete, the '/logout' request does log the user out.
    If I set invalidate_session to true, the '/logout' request does log out the user, irrespective of whether the page has completely loaded or not.

    I am wondering if there could be a race condition with a concurrent request writing the user's token to the session just after the /logout request has removed it, but this is very much a guess, at this point.

    I'll continue debugging, and try stepping through the logout process, to see if I can pin down what's going on!

  • 2017-08-03 Patrick Vale

    Hi Ryan,
    Thanks for another great tutorial!
    I wondered if I could ask your advice about logging users out from a two-firewall setup?
    The setup is the one discussed in this thread

    I have a situation where I would like to be able to log users out of one firewall (by clicking the 'logout' link generated for that firewall), but not log them out of the others at the same time.

    I wanted to check that I was going about it in the right way, before going down a rabbit hole on this!

    I've set the 'invalidate_session' parameter in the logout key of the firewall config to 'false' for both firewalls


    public_users:
    anonymous: ~
    logout:
    path: /logout
    invalidate_session: false
    admin_users:
    pattern: ^/admin
    anonymous: ~
    logout:
    path: /admin/logout
    target: /admin/login
    invalidate_session: false

    However, when I try to logout, it doesn't always actually log the user out. Looking into the session, it seems that the '_security_public_users' element of the '_sf2_attributes' key isn't being removed.

    Looking at the documentation, it seems to suggest that this should be removed, even when 'invalidate_session' is set to 'false', but that doesn't seem to be happening in every case within my application.

    Should I be handling the removal of the '_security_public_users' key from the session within a custom logout handler?

    The full security.yml is available in this pastebin, for reference.

    Many thanks,
    Patrick

  • 2017-05-30 Victor Bocharsky

    Good catch! Glad you got it working.

    Cheers!

  • 2017-05-29 Imogen Hallett

    FYI, Found the problem - simple type in SecurityController. Thank you for comments.

    $form = $this->createForm(LoginForm::class,[
    'username' => $lastUsername,
    ]);

    SHOULD BE

    $form = $this->createForm(LoginForm::class,[
    '_username' => $lastUsername,
    ]);

    NOTE '_username' NOT 'username'

  • 2017-05-29 Imogen Hallett

    Hi...

    If I do either of these

    $data = $form->getData();
    $request->getSession()->set(
    Security::LAST_USERNAME,
    $data['_username']
    );

    dump($data['_username']); die;
    return $data;

    OR

    $data = $form->getData();
    $request->getSession()->set(
    Security::LAST_USERNAME,
    $data['_username']
    );

    dump($request->getSession()->get(
    Security::LAST_USERNAME
    ));die;
    return $data;

    I get the username dumped to the screen... No idea how to debug beyond this... Any help would be much appreciated. Thank you.

  • 2017-05-29 Victor Bocharsky

    Hey Imogen,

    Hm, are you sure the $data['_username'] doesn't empty when you set it into session? Could you make sure with dump($data['_username']); before setting it? Also ensure you print the last username in Twig template of your login form.

    AFAIK, since Symfony 2.6 a new security error helper was added - check the example how to use it: http://symfony.com/blog/new...

    Does it help you?

    Cheers!

  • 2017-05-28 Imogen Hallett

    Hi

    Firstly, thank you for this amazing course.

    Secondly, for some reason

    $request->getSession()->set(
    Security::LAST_USERNAME,
    $data['_username']
    );

    is not prefilling _username after an invalid login attempt. All else seems to be working fine. Any ideas? I am running Symfony 3.1.4

  • 2017-05-17 Diego Aguiar

    NP man, it has been fun replying to your questions :)

    Cheers!

  • 2017-05-17 Terry Caliendo

    Thanks for the response and the reference.

    I had previously overlooked this page, but I also now see the key in here.
    http://symfony.com/doc/curr...

    Thanks again!
    Terry

  • 2017-05-16 Diego Aguiar

    Hey Terry Caliendo

    You can specify to which path you want to redirect them by setting up the target key in your security.yml, by default it's set to "/"


    security:
    firewalls:
    main:
    logout:
    target: /your/custom/path

    But, if you want something more dynamic, you will have to create your own "LogoutHandlerListener", it's almost a regular listener but it has to implement "LogoutHandlerInterface", you can follow what this guy did here: http://stackoverflow.com/a/...

    Have a nice day!

  • 2017-05-15 Terry Caliendo

    How do you control which page the user is redirected to after being logged out by the system?

  • 2017-05-03 Julia Shishik

    Oh, thank you very much! Yes, I had an error in the method getCredentials. I missed the exclamation mark befor $isLoginSubmit! Oh yeah. It is sad! But now everything is fine

  • 2017-05-03 Victor Bocharsky

    Hey Julia,

    Good investigation! getCredentials() called on every request and your job is to read the credentials from the request and return it. If you return null, the rest of the authentication process is skipped, i.e. getUser() won't be called. Otherwise, getUser() will be called and the return value is passed as the first argument.

    So make sure you *return* credentials in getCredentials() method.

    Cheers!

  • 2017-05-02 Julia Shishik

    Good morning!
    getCredentials() method is being called.

    getUser() is NOT called((
    Logged in asanon.
    AuthenticatedYes
    Token classAnonymousToken
    Firewall namemain

    and 2 missing translation, and log message

    INFO
    21:51:37
    request Matched route "security_login".
    Show context
    INFO
    21:51:37
    security Populated the TokenStorage with an anonymous Token.
    WARNING
    21:51:37
    translation Translation not found.
    Show context
    WARNING
    21:51:37
    translation Translation not found.
    Show context
    Last 10

  • 2017-05-01 weaverryan

    Hey Julia Shishik!

    I can definitely help :). So if you're seeing *nothing* - like no errors in any situation - then it's possible that your authenticator is not being called at all! Here's what you can do to check. First, put var_dump('here!');die; at the top of your getCredentials() method. Is this being called? If not, check your services.yml and security.yml setup. If you configure your authenticator correctly, the getCredentials() method should be called on *every* request. If the method IS being called, check your logic in getCredentials() to make sure you're checking the current URL correctly. Also, verify that getUser() is being called when you submit. If getUser() is called, then, at the very least, you should *definitely* see an error message when you type in a wrong user/pass. So, the problem is probably somewhere before this.

    Let me know what you find out!

    Cheers!

  • 2017-04-29 Julia Shishik

    Good evening! Can you help me, please! I can't login. I enter the correct information nothing happens, I enter incorrect information - the same thing

  • 2017-04-12 Victor Bocharsky

    Hey Moudug,

    Actually, it's a browser feature only. So if you're using public computer, you just have to clear the history after using it and that should fix the problem. Not sure, but maybe just closing browser tab will be enough too, it depends on a browser you're using I think.

    Cheers!

  • 2017-04-11 Moudug

    Yes you're right, I am indeed logged out but still abble to see only the last page I visited.
    I googled before asking, but wanted to know if there was an official technique to fix it.
    Thanx Ryan.

  • 2017-04-11 weaverryan

    Hey Moudug!

    Hmm. So it's not exactly what you're saying :). Actually, when you hit back, you are still logged out - but your browser is showing your the previous page, as it looked when you were there a moment ago. If you try to click on any links that require login, you'll go to the login page (because you're not actually logged in).

    If you google the issue, there are some solutions - but it's just not something that's super well supported. One decent solution might be to run some JavaScript on an interval (e.g. every 30 seconds + on page load). In that JS, make an AJAX call to your server to see if the user is logged in. If they are not, redirect to the login page. Then, even if the user has 10 browser tabs open, if you logout in one of them, the other tabs will go to the login page after a few seconds.

    Cheers!

  • 2017-04-11 Moudug

    I have the following behavior : after logging out, I hit the back button... and then I am logged again.
    That seems pretty annoying, but I think this is a quite common issue that has a solution.
    What is the best way to fix this ?

  • 2017-04-05 Diego Aguiar

    Hey Terry!

    It's an interesting question, and actually, yes, you can!
    You just have to write the route's name instead of the path e.g.


    //security.yml
    logout:
    path: logout_name

    Of course, if you have to change the route's name, well you need to change it in here too, but that's less likely to happen.

    Have a nice day!

  • 2017-04-04 Terry Caliendo

    Small thing, but I'm just curious... Is there any way to not hard code the "/logout" route in "security.yml"? Can you pull in the "security_logout" route name somehow so that if you later change the url of the logout, you don't have to change it in both the SecurityController and security.yml?

  • 2017-01-23 weaverryan

    Hey Stan!

    Ah, great question! So, let me say a few things:

    A) We didn't redirect from POST /login to GET /login for any special reason - this has always been Symfony's default behavior (but I can't think of any advantage to doing this).

    B) So, you don't *need* to redirect - you could allow the request to continue like normal (you would simply *not* request a Response from onAuthenticationException). If you did this, then you could just read the last username off of the request.

    If you use Symfony's built-in form_login functionality, they actually allow for both, and it's configurable. So, do whatever you want - you're definitely thinking about the situation correctly: there is no special reason for redirecting versus just allowing the request to continue that I can think of.

    Cheers!

  • 2017-01-22 Stan

    Why do we store last username is session? Why don't get it from POST params? We didn't redirect anywhere so they should still stay there?

    UPD. Well no, we was redirected from POST /login to GET /login. Why?

  • 2016-09-29 Victor Bocharsky

    Hey Yang,

    Use global `app` variable to access user object, i.e. {{ app.user.username }}. But ensure your user is authenticated, otherwise you'll get an error:

    {% if is_granted('IS_AUTHENTICATED_ANONYMOUSLY') %}
    <li>Login...</li>
    {% else %}
    <li>< a href="{{ path('security_logout') }}">Logout</li>
    {% endif %}

    Cheers!

  • 2016-09-13 Victor Bocharsky

    Ah, then it makes sense! Yes, you're definitely right here! :)

  • 2016-09-13 Yang Liu

    I think theres a small missunderstanding here, I want to output the username, not to check. because the original <li> codes are wrapped in a if-block already {% if is_granted('ROLE_USER') %}. I thought users only get a role if they are logged in, so this already check if user is logged in. So if it's true, I can output the username by simplying adding {{app.user.username }} in front of the logout-button.

  • 2016-09-13 Victor Bocharsky

    Be careful! You can failed with error when user IS null if you just checking `app.user.username ` Use extra check in if statement like:
    {% if app.user and app.user.username %}

    But probably `if app.user ` will be enough here, because for some weird reasons user could have no username but could be logged in. I mean, that app.user return an object only if a user is logged in. So you don't need username extra check here.

    Cheers!

  • 2016-09-13 Yang Liu

    that was fast!! good to know, thx, I modify the original code and added {{ app.user.username }}, it works nicely. thx

  • 2016-09-13 Victor Bocharsky

    Hey Yang,

    It's easy enough! You have access to the global context in Twig templates which allows to get currently logged in user with `app.user`. So just use:


    <li>
    {% if app.user %}
    <a href="{{ path('security_logout') }}>Logout</a>
    {% else %}
    <a href="{{ path('security_login') }}>Login</a>
    {% endif%}
    </li>

    Cheers!

  • 2016-09-13 Yang Liu

    when a user is logged in, I want to show something like "username loggout-button" in the navbar. But since this is in the base.html.twig:
    <li>< a href="{{path('security_logout') }}>Logout</li>
    I have no idea how to access the username
    First attempt: I tried to modify loginAction():
    return $this->render(
    'security/login.html.twig',
    [
    'form' => $form->createView(),
    'error' => $error,
    'username' =>$lastUsername,
    ]
    );
    and in bast.html.twig:
    {{ username }}<li>Logout</li>
    but like I expect, I got the following error message when I try to login:

    Variable "username" does not exist in base.html.twig at line 26

    then I thought of using block in base.html.twig and move the {{username}} part to the login.html.twig, but it doesn't seems to be the right solution either cause I will have to use it everywhere...
    so, can you help me with this?

    edit: just realized that what I tried couldn't have worked because login.html.twig is only the loginform, and has nothing to do with the pages after I logged in. So, now I have absolutly no idea how to solve this^^

  • 2016-09-13 Yang Liu

    when a user is logged in, I want to show something like "username loggout-button" in the navbar. But since

    <li>< a href="{{ path('security_logout') }}">Logout</li>

    is in the base.html.twig, I have no idea how to access the username.
    I tried modify loginAction():
    return $this->render(
    'security/login.html.twig',
    [
    'form' => $form->createView(),
    'error' => $error,
    'username' =>$lastUsername,
    ]
    );
    and in bast.html.twig:
    {{ username }}<li>Logout</li>

    but like I thought, this gives me the message:
    Variable "username" does not exist in base.html.twig at line 26

    so how can I do this? I was thinking about making blocks in base.html.twig and move the line to security/login.html.twig where I CAN access the username property, but doesn't seem to be the right way either...

  • 2016-09-13 Yang Liu

    ok, that solved the problem. Found out that when I autocompleted Security, I accidently added the use statement
    Sensio\Bundle\FrameworkExtraBundle\Configuration\Security
    thx

  • 2016-09-12 weaverryan

    Yo Yang!

    Make sure you have the use statement for the Security class at the top of your file - it should be:


    use Symfony\Component\Security\Core\Security;

    I'm going to update our code blocks on this page - it wasn't highlighting that this was needed :).

    Cheers!

  • 2016-09-12 Yang Liu

    hm... I get

    Undefined class constant 'LAST_USERNAME'

    on the line:

    $request->getSession()->set(Security::LAST_USERNAME, $data['_username']);

  • 2016-08-15 Lee Ravenberg

    I ran into the problem where the last_user value wasn't saved into the session. This was due to the logic that I adjusted.

    Tutorial logic:
    $isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST');
    if (!$isLoginSubmit) {
    return;
    }

    My Logic:
    if (!$request->getPathInfo() == '/login' && !$request->isMethod('POST')) {
    return;
    }

    So I didn't see what went wrong and Ryan had a look at it. He pointed out that I *slightly* reversed the logic. Then he added:

    "For example, if you go to /login with a GET request (e.g. you submit the form, then symfony redirects you back to /login), I think it will skip your if statement. In this case, you call handleRequest(), but since there is no login information, $form->getData() is blank. This is then being set into the session, clearing out any LAST_USERNAME from the previous POST request. Later on the request, your controller is rendered and this is blank!"

    I think this helps anyone that run into the same issue :P

  • 2016-07-26 weaverryan

    Cheers! :)

  • 2016-07-26 Andjii

    Already solved. Sorry for disturbance. Your course is amazing!

  • 2016-07-01 Victor Bocharsky

    Hey there!

    New videos are added!

  • 2016-06-30 Victor Bocharsky

    Hey, Matthew!

    Yes, you're right, it's not ready yet. But it will be very soon!

  • 2016-06-30 Matthew Thomas

    Oh man was enjoying this. Seems the next load of videos are not ready????