If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
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!
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!'); | |
} | |
} |
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') %}
:
<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')
:
<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.
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!
ok, that solved the problem. Found out that when I autocompleted Security, I accidently added the use statement
Sensio\Bundle\FrameworkExtraBundle\Configuration\Security
thx
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
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!
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.
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'
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.
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!
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?
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!
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!
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
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...
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!
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?
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!
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
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 ;)
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
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!
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^^
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!
that was fast!! good to know, thx, I modify the original code and added {{ app.user.username }}, it works nicely. thx
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!
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.
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
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!
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
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!
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
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
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!
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 ??
Hey Mehdi,
Good questions! There're a few possible solutions:
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!
Hello,
I am sorry for the late reply.
I used the first solution and it worked for me.
Thank you Victor
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")?
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 :)
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 ?
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!
// 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
}
}
hm... I get
Undefined class constant 'LAST_USERNAME'
on the line:
$request->getSession()->set(Security::LAST_USERNAME, $data['_username']);