Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Logging out & Pre-filling the Email on Failure

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

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:

... 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']:

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

... 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":

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

... 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') %}:

<!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'):

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

61
Login or Register to join the conversation
Default user avatar
Default user avatar Yang Liu | posted 5 years ago

hm... I get

Undefined class constant 'LAST_USERNAME'

on the line:

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

1 Reply

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!

2 Reply
Default user avatar

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

Reply
Default user avatar

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

1 Reply
Default user avatar
Default user avatar Imogen Hallett | posted 5 years ago

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

1 Reply

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-in-symfony-2-6-security-component-improvements#added-a-security-error-helper

Does it help you?

Cheers!

Reply
Default user avatar
Default user avatar Imogen Hallett | Victor | posted 5 years ago

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.

Reply
Default user avatar

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'

Reply

Good catch! Glad you got it working.

Cheers!

Reply
Default user avatar
Default user avatar Matthew Thomas | posted 5 years ago

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

Reply

Hey, Matthew!

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

Reply

Hey there!

New videos are added!

Reply
Default user avatar

Hmm. I'm a little creeped out by AuthenticationUtils.php
How did getLastUsername() get in there? What am I missing or forgetting? Was it there all along? Did something we've done up to this point cause Symfony or PHPStorm to put it in there? If so...what is that behind-the-scenes code generation process, and what do I need to be aware of to make sure it behaves properly, i.e. I decide to call my user identifiers 'aquanautname', instead of username?
"Automagic" stuff like that is usually what gives me headaches in Rails.

Reply

Hey Adam!

Sorry for the late response, for some reason this message went directly to the spam folder =S

At the beginning it migh look's like magic, but it's not, all that `AuthenticationUtils::getLastUsername()` does, is fetch a value from the session, in this case it's empty because Symfony doesn't know how to fill it, thats why we manually set it with the value of the username input. Maybe if you rewatch the very first and half minute you will get it.

Cheers!

Reply
Default user avatar
Default user avatar Terry Caliendo | posted 5 years ago

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?

Reply

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!

Reply
Default user avatar
Default user avatar Terry Caliendo | posted 5 years ago

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

Reply

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/39293401

Have a nice day!

Reply
Default user avatar
Default user avatar Terry Caliendo | MolloKhan | posted 5 years ago

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

Reply

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

Cheers!

Reply
Default user avatar
Default user avatar Yang Liu | posted 5 years ago

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

Reply

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!

Reply
Default user avatar
Default user avatar Jovana Gajic | posted 5 years ago

Hey, great job on these tutorials, you're really helping me a lot. I'm having a problem with this one. When I try logging in, it automatically logs me out. However, if I remove code that checks if the user is logged in base.html.twig everything goes smoothly. Do you have any idea why this might be happening?

Reply

Hey Jovana Gajic

How are you checking that you are logged in, in your template?
Can you check at your profiler bar if it says "anon." after logging in?

Cheers!

Reply
Default user avatar
Default user avatar Jovana Gajic | MolloKhan | posted 5 years ago

if I type this {% if is_granted('ROLE_USER') %} it redirects me back to the login page. If I omit this part, I get logged in, and profile bar shows my username. When I'm redirected to login page it shows anon in profile bar

Reply

Ohh, this is interesting. I believe the is_granted() function is doing something nasty. Can you tell me which version of Symfony are you using?

Try changing your if statement to this:


{% app.user and is_granted('ROLE_USER') %}

And let me know if it works ;)

Reply
Default user avatar
Default user avatar Jovana Gajic | MolloKhan | posted 5 years ago

Hey, I've finally made some progress. I figured out that I was logged in but wasn't authenticated. At one moment I've made my model implement the Serializable, as described in Symfony documentation, and once I removed it, along with the serialize and the unserialize methods it worked. I suppose those methods for serializing, unserializing weren't good for this case

Reply

Hey Jovana,

If you were logged in but were not authenticated - probably you have some misconfiguration of your firewall. If you still have a problem, could you show us your "security.firewalls" section in app/config/security.yml ? Or also there could be a problem in your serialize()/unserialize() methods implementations, probably you unserialize incorrectly (incorrect fields order, etc.), but it's difficult to say without seeing your implementation of those methods.

Cheers!

Reply
Default user avatar
Default user avatar Yang Liu | posted 5 years ago

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^^

Reply

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 %}
    &lt;a href="{{ path('security_logout') }}&gt;Logout&lt;/a&gt;
  {% else %}
    &lt;a href="{{ path('security_login') }}&gt;Login&lt;/a&gt;
  {% endif%}
</li>

Cheers!

Reply
Default user avatar

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

Reply

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 <i>only</i> if a user is logged in. So you don't need username extra check here.

Cheers!

Reply
Default user avatar

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.

Reply

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

Reply
Default user avatar
Default user avatar Julia Shishik | posted 5 years ago

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

Reply

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!

Reply
Default user avatar
Default user avatar Julia Shishik | weaverryan | posted 5 years ago

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

Reply

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!

Reply
Default user avatar
Default user avatar Julia Shishik | Victor | posted 5 years ago

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

Reply
Default user avatar
Default user avatar Lee Ravenberg | posted 5 years ago

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

Reply
weaverryan Avatar weaverryan | SFCASTS | posted 5 years ago | edited

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!

Reply
mehdi Avatar

Hello,
Thank you for all your effort.
When I am logged in, I type /login in the address bar, it rendered the login page. I think it should not appear, instead another page ( hompepage) should. How to do this ??

Reply
Victor Avatar Victor | SFCASTS | mehdi | posted 5 years ago | edited

Hey Mehdi,

Good questions! There're a few possible solutions:

  1. Just add one more extra check in the beginning of loginAction(), and if user is logged in - redirect him to homepage.
    
    

if ($this->getUser()) {

return $this->redirectToRoute('homepage');

}



Probably this one is the best.

2. Or, add if statement in your loginAction() and if user is logged in - *render* homepage, if not - render login page. But in this case you will show home page under the "/login" URL which is a bit weird, so I'd prefer the 1st solution :)

Cheers!
Reply
mehdi Avatar

Hello,
I am sorry for the late reply.
I used the first solution and it worked for me.
Thank you Victor

Reply

Hey Mehdi,

No problem! I'm glad you got it working for you ;)

Cheers!

1 Reply
Markus B. Avatar
Markus B. Avatar Markus B. | posted 5 years ago

How can I redirect every visitor which is not logged in to the login page?
And how can I than make exceptions to this rule (for a page like "About this website")?

Reply

Hey Markus B.
One way of achieving it is via an "Access control list", you can configure your own inside the security.yml file
You can find more detailed information here: https://symfony.com/doc/current/security/access_control.html
This is a basic example of securing all pages but login


    access_control:
        - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY  }
        - { path: ^/, roles: IS_AUTHENTICATED_FULLY }

Have a nice day :)

Reply
Default user avatar

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 ?

Reply

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!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}
userVoice