If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
If I submitted this form right now, it would register me, but it would not actually log me in... which is lame. Let's fix that.
This is always pretty easy, but it's especially easy because we're using
Guard authentication. Inside of UserController
, instead of redirecting to the
home page: do this: return $this->get()
to find a service called
security.authentication.guard_handler
. It has a method on it called
authenticateUserAndHandleSuccess()
. I'll clear the arguments and use multiple lines:
... lines 1 - 10 | |
class UserController extends Controller | |
{ | |
... lines 13 - 15 | |
public function registerAction(Request $request) | |
{ | |
... lines 18 - 20 | |
if ($form->isValid()) { | |
... lines 22 - 29 | |
return $this->get('security.authentication.guard_handler') | |
->authenticateUserAndHandleSuccess( | |
... lines 32 - 35 | |
); | |
} | |
... lines 38 - 41 | |
} | |
} |
The first argument is the $user
and the second is $request
:
... lines 1 - 10 | |
class UserController extends Controller | |
{ | |
... lines 13 - 15 | |
public function registerAction(Request $request) | |
{ | |
... lines 18 - 20 | |
if ($form->isValid()) { | |
... lines 22 - 29 | |
return $this->get('security.authentication.guard_handler') | |
->authenticateUserAndHandleSuccess( | |
$user, | |
$request, | |
... lines 34 - 35 | |
); | |
} | |
... lines 38 - 41 | |
} | |
} |
The third argument is the authenticator whose success behavior we want to mimic.
Open up service.yml
and copy the service name for our authenticator:
... lines 1 - 5 | |
services: | |
... lines 7 - 17 | |
app.security.login_form_authenticator: | |
class: AppBundle\Security\LoginFormAuthenticator | |
autowire: true | |
... lines 21 - 27 |
In the controller, use $this->get()
and paste the service ID:
... lines 1 - 10 | |
class UserController extends Controller | |
{ | |
... lines 13 - 15 | |
public function registerAction(Request $request) | |
{ | |
... lines 18 - 20 | |
if ($form->isValid()) { | |
... lines 22 - 29 | |
return $this->get('security.authentication.guard_handler') | |
->authenticateUserAndHandleSuccess( | |
$user, | |
$request, | |
$this->get('app.security.login_form_authenticator'), | |
... line 35 | |
); | |
} | |
... lines 38 - 41 | |
} | |
} |
Finally, the last argument is something called a "provider key". You'll see that occasionally - it's a fancy term for the name of your firewall:
... lines 1 - 2 | |
security: | |
... lines 4 - 14 | |
firewalls: | |
... lines 16 - 20 | |
main: | |
anonymous: ~ | |
guard: | |
authenticators: | |
- app.security.login_form_authenticator | |
... lines 26 - 40 |
This name is almost never important, but it actually is in this case. We'll say main
:
... lines 1 - 10 | |
class UserController extends Controller | |
{ | |
... lines 13 - 15 | |
public function registerAction(Request $request) | |
{ | |
... lines 18 - 20 | |
if ($form->isValid()) { | |
... lines 22 - 29 | |
return $this->get('security.authentication.guard_handler') | |
->authenticateUserAndHandleSuccess( | |
$user, | |
$request, | |
$this->get('app.security.login_form_authenticator'), | |
'main' | |
); | |
} | |
... lines 38 - 41 | |
} | |
} |
And we're done!
Now, this will log us in, but it also has a bonus super-power. Right now, we're
anonymous. So let's try to go to /admin/genus
. Of course, it bounces us to the
login page. That's fine - click to register. Use weaverryan+20@gmail.com
,
add a password and hit enter.
Check this out. Well, ignore the access denied screen.
First, it did log us in. And second, it redirected us back to the URL we were trying to visit before we were sent to the login page. This is really great because it means that if your user tries to access a secure page - like your checkout form - they'll end up back on the checkout form after registration. Then they can keep buying your cool stuff.
Of course, we see the access denied page because this user only has ROLE_USER
and
the section requires ROLE_MANAGE_GENUS
.
Ok guys, that's it. Yes, you can get more complicated with security, especially authentication. And if you need to check permissions that are object specific - like I can edit only genuses that I created - then check out Symfony's awesome voter system. Hey, we have a course on it!
But for the most part, you guys have the tools to do really incredible things. So go out there, build something awesome and tell me about it.
Seeya next time!
Hey Boran,
We're planning to add the level of difficulty for each course, but while we haven't added it yet - here's my rough gradation for these courses:
But I think you can get a rough estimate by yourself: the easiest courses have beginner level, the hardest courses have advanced ;)
Cheers!
Hallo ,how i can make facebook google twitter login authentication and authorization with out using FOS Bundle , Is there a special bundle for doing that
Hi Boran,
Yes, it is! Check the knpuniversity/oauth2-client-bundle. Actually, with this bundle you can do social networks authentication/authorization with or without FOSUserBundle. But if don't want to use FOSUserBundle - you will have to implement User by yourself, i.e. implement UserInterface.
Cheers!
Hi again, I would like to know how can I logout the active user after "n" minutes of innactivity. I found this information from here -> https://symfony.com/doc/cur....
It says: "The cookie_lifetime would be set to a relatively high value, and the garbage collection gc_maxlifetime would be set to destroy sessions at whatever the desired idle period is".
My questions will be:
1- if I do that in the server side, it will apply to all apps that are running in this apache, isn't? What about if I just want to apply this change to one app only? I would like to configure this autologout behaviour just in one app.
2- Will affect this to all sessions? I mean, beside the user session, the rest of sessions I'm using for the app, they will be deleted it aswell, isn't it? What about if I just want to delete the user sessions when there is inactivity and keep the rest operative?
Hey Apr
About your first question you're right, if you change PHP's gargabe collection lifetime, it will apply to all PHP apps running on that server
About your second question, it should only delete sessions that have been inactive for the specified life-time.
What I would do is to implement the "auto-logout" functionality via JS. I would add some JS code that detects user's inactivity and after "n" minutes I would ping the server to tell it to logout that user.
Cheers!
The solution by JS is so clever, thanks. I just saw another possible solution. What do you think about this one?
https://stackoverflow.com/q...
What problems would have managing by this way? Will work the SessionIdleHandler answer? So the functionality will be that if a user call a controller after "x" seconds will destroy the user sesion and redirect to the login page. Am I wrong?
Ohh, that's an interesting implementation. I think it can work but I'm not sure about $this->session->getMetadataBag()->getLastUsed()
I would double-check first if you can rely on it (it seems like you can). Have you seen what "9Gag" does, they ping you when you have been inactive, I find the logic very similar to what you need
Cheers!
I have just finished this course, I'm a little stuck on the redirects. The form submits and logs the user in but I then get redirected to the registration page which I have redirecting to the home page of my site.
I'm unsure where/how I should I handle redirecting to the page prior to the registration form? Have I missed a step somewhere?
Hey Sean!
Hmm, ok, let's figure this out :). There are 2 places to look:
A) Make sure you have a return statement on the line where you call the authenticateUserAndHandleSuccess() method. This probably isn't the problem (you mentioned it *redirected* to the registration page, but just in case it was actually just re-rendering) - but had to mention it!
B) When you call the authenticateUserAndHandleSuccess() method on your authenticator, that is calling the onAuthenticationSuccess() method on your authenticator to determine where to redirect you to. In this tutorial, we never create this method - because we extend AbstractFormLoginAuthenticator, which takes care of this for us (specifically, it checks to see if the user should be redirected back to some secure page, and if there is none, it calls your getDefaultSuccessRedirectUrl() method). However, in Symfony 3.1, there's a slight change where you can implement the onAuthenticationSuccess() method directly yourself. If you do this, then you need to take care of redirecting back to the previous, secure page yourself. But, there's a handy new TargetPathTrait to help you with this (some details here https://knpuniversity.com/s....
Let me know what you find out, and I'll be happy to offer any other help I can!
Cheers!
Thanks for the response, I moved past this and have been working on other things, so I haven't had a chance to try this out yet. I will let you know how I get on if I find any thing useful.
Actually I realise my case is a little different, the user has access some of the content on the page prior to login, so most pages don't redirect to the login form for sign in, which is how I think this example works, so it could be working how it was intended to.
Yep, you're absolutely right. So, if the user will click "login" (i.e. not try to access a protected page, and be forced to /login) and then you want it to redirect back to the previous page, you can still do this by setting the redirect URL manually. A good example of that is actually from our Symfony2 version of the tutorial: https://knpuniversity.com/s.... Basically, you manually add the URL you want the user to be sent back to into the session, so that it's there later when your authenticator finishes login. We do this on KnpU on our registration page, to keep you flowing smoothly :).
Cheers!
Hi, i have one little problem.
I've written this :
return $this->get('security.authentication.guard_handler')
->authenticateUserAndHandleSuccess(
$user,
$request,
$this->get('sng.user.security.login_form_authenticator'),
'main'
);
And the is my error : Détails de l'erreur : Type error: Argument 3 passed to Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken::__construct() must be of the type array, string given, called in /Applications/MAMP/htdocs/shootandgo-dev/vendor/symfony/symfony/src/Symfony/Component/Security/Guard/AbstractGuardAuthenticator.php on line 38
I don't know why...I'm on v2.8, is it normal to have this exception with this version ?
thanks
Hey Christophe,
It's weird error. Could you try to update Symfony to the latest 2.8 version, it's 2.8.14 for now? Most likely this error should be gone. Let me know if it helps.
Cheers!
Hi Victor,
Sorry i've said 2.8 but i'm on 2.8.14 in reality, and yes it's a weird error, i don't understand what happen
Hey guys!
I believe the problem is in the getRoles()
function in your User
class - it looks like that method is returning a string instead of an array (that's the cause of the error). Double-check that method and make sure it's always returning an array, even if it's an array with just one item :).
Cheers!
Hi ryan ^^,
Well i think it's that too, but my GetRoles() return an array...
public function setRoles(array $roles)
{
$this->roles = $roles;
}
public function getRoles()
{
$roles = $this->roles;
if(!in_array('ROLE_USER',$roles)){
$roles[] = 'ROLE_USER';
}
return $roles;
}
And with that, i can logging normally (not automatically) but i can't register because i raise an exception : Warning: in_array() expects parameter 2 to be array, null given -_-
I think i will stop coding and going back to school to learn how paint the wall. T_T
Ah, your latest error makes sense... What about your User::$roles property declaration? It should be like this:
class User {
private $roles = [];
}
I suppose it'll also fix your first error too ;)
P.S. Please, double check you don't set any non-array value to the $this->roles in the User::_construct() method or in any other method.
Cheers!
1. Why am I getting a error with the db connection refused ?
2. Why is the symfony dev page looking so wierd?
a picture of the error and the dev page
http://imgur.com/a/R8hli
Hey @hannesstefani ,
1) Please, double check your DB credentials in `app/config/parameters.yml`. If this file doesn't exist - execute "$ composer install" in your terminal which creates it for you and then fill it with right credentials.
2) I believe executing "$ composer install" without errors fix this styles. Actually, composer run another Symfony command behind the scene: "$ bin/console assets:install" which install some Symfony built-in CSS styles to make it looks prettier. However, if you got some errors during the composer install, this command was omitted. So you should run this command manually and it'll fix the styles. Also, try to clear the cache.
Cheers!
1) i double checked my connection and if my db server is running and also my parameters file but still the same error. Btw the db server is running on another virtual machine so it is not installed with the localhost. i changed the ip and login credentials to my needs to that server, but it always throws me the same error...
could there be something wrong with prot worwarding? but both machines are running in the same network and all ports are open in my network...
2) i still dont get the nice style from symfony. i cleared the cache in dev and in prod it throws me an error with connection refused 2002 SQLSTATE. So i think there must be something wrong with the db connection...
after clearing the cache i still get the same output as in the picture i recently uploaded.
Hey Blueblazer172!
1) Yea, something is definitely wrong with the database configuration... the question is what is the problem!? I would recommend trying to connect to the database from your host machine via some other method to check the credentials. For example, you might use the command line to try to connect - e.g. mysql -u root -h 192.168.1.100
(obviously, use your real IP address!). Or, you could use something like Navicat or Sequel Pro. I imagine that you will also get a "connection refused" error when you try this. But then, it may be easier to debug! I do think that port forwarding might be a problem, or the port just might not be open on your virtual machine: "Connection Refused" means that the server completely rejected the connection - you get this exact error when you try to connect to a machine on a port that is not open.
2) Once you solve issue (1), try running bin/console assets:install
again. Right now, the database problem is blocking this from completing. But once that works, this command will work and BOOM, you will have the nice error styling you deserver.
And sorry for my late reply! Good luck and cheers!
Okay i made a silly mistake;)
I didn't open the connection from my MySQL server.
So what i did I changed the bindadress in the config file (btw I'm using Maria DB and only this one is a bit different than others) to 0.0.0.0 so that I could be attached to any ip.
Maybe this helped others with the same problem. Now everything is working;)
Hey Ryan and all,
great tutorial.
I like to extend the the functionality with:
- Password forgotten link
- User profile admin area
How could I do that?
Or would it then be better to use the FOSUserBundle?
Any recommendations are welcome.
Hey Gunnar!
This is a tricky topic. FOSUserBundle is something that I hope we can make "obsolete" in the Symfony world (by creating other, smaller tools). It gives you great functionality quickly, but it makes so many things much more difficult after.
For the user profile admin area, that's simple enough: just create your own custom routes/controllers & form to make an area where an admin can modify the users. You could even use EasyAdminBundle if you want something quickly that works well.
For the forgotten password link, that is something that FOSUserBundle does really well... but I still don't think it's worth using it. To create it manually, you would make a system that look something like this:
1) Create a route/controller/template where the user submits their email address
2) On submit, if that email exists in the database, then do 2 things:
A) Generate a long, random string and set it on a new field on your user - e.g. passwordResetToken. You will probably also want a passwordTokenExpiresAt DateTime field that you set for 24 hours from now (so that the token cannot be used forever).
B) Send the user an email with a link that includes that token (see next step for details about the route that you link to)
3) Create a new route/controller that will handle when the user clicks the link in their email. It might look like /forgot-password/{token}. Inside, you would look up the User object based on this token (and also make sure the token hasn't expired). If it is all valid, then you would basically render a reset password form. Make the user re-submit back to this same URL (with the token) and update their password on submit. Then, make sure to clear out (set to null) the passwordResetToken so it can't be used again.
About step (3), if you want, when the user originally clicks the link, you could actually *authenticate* the user manually (https://knpuniversity.com/s..., nullify the token, the simply redirect them to your "change password" form. It's a bit simpler, and the user is automatically authenticated after resetting their password.
I hope this helps! In the future, I hope we'll have some code generators or a smaller bundle to help with the reset password.
Cheers!
I have been hunting this information down for about an hour, of course The Ryan would have a solution!
To be honest, when I first started using Symfony Casts (before the name change) I disnticly remember a tutorial that you did where there was some copy and pasting of email confirmation tokens to check the code, but for the life of me I couldn't find it on the site. Clearly You guys thought "bleh, no-one wants that any more!" well i am here to tell you that I am one of those people. Anyway your description above is pretty clear, thanks mate.
Hey Peter!
Ha! Glad you find this! Btw, we will probably have a forgot password MakerBundle command very soon: https://github.com/symfony/...
Cheers!
Ooo.. exciting, I will probably have implemented someone by then, but glad to see you guys are working hard on making that maker bundle even more awesome!. Will keep my eye out for it.
I did that a while ago, but there are a lot of useless features I added just for fun: https://github.com/giorgiop...
This is a bare-bones Symfony 4 version with bootstrap 4, which I plan to use for my future projects at work https://github.com/giorgiop...
Can I congratulate with you?
In the same way, I did a bare project doing it as minimal and perfect as possible and I it is very similar to yours. But without automatic login after user creation. I appreciated a lot your job.
Hello everybody,
great content as usual. I'm doing the same in Symfony 4 and everything is working great; however, I'd like to know if there is an easy way to automatically login the user without writing a custom login form authenticator, because I in my new project I'm trying to use form_login (which is working great), and I don't want to have a custom authenticator just for this. Or should I stick to using only my custom authenticator?
Thank you!
Hey @disqus_XWG6Op03gN
I searched for other ways for login in a user, but couldn't find an easier way than using a GuardAuthenticatorHandler, so I would say, yes, create your own and then forget about it :)
Cheers!
Thank you. I've got problems with onAuthenticationSuccess though, I could just call the parent in Symfony 3, but in Symonfy 4 I get this: "Cannot call abstract method Symfony\Component\Security\Guard\AuthenticatorInterface::onAuthenticationSuccess()." Doing this was enough to redirect the user to the page he was trying to visit before being redirected to the login page, but now it doesn't work anymore. I'm extending AbstractFormLoginAuthenticator in both projects.
EDIT: ups! this was answered two years ago http://disq.us/p/19nhktf
Hi !
Thanks to Knp I now have a fully functional login and registration system through Guard. BUT...But now I have an issue. I want to have both on the home page (and a facebook button, but it works fine). With twig render I can have both forms. In my routing file, I set the paths to "/" (and in my LoginFormAithenticator -> getCredentials too). Login works, but not register (it reloads home). If I set the register path back to "/register" it works fine, but not on the home page.
What can I do to solve this ?
Thank you.
Edit : I thought using Ajax could solve my problem (and be prettier) so I searched on your site and found this ressource https://knpuniversity.com/s... but I have troubles to achieve the very first sentence "Suppose your login form uses AJAX." Is there any article on that part ?
Hey Jean-tilapin!
If I understand you correctly, you have your login and registration forms on your homepage. And, the login form works but the registration form does not work. Is that correct?
Where do you have the registration form submit handling code (the $form->handleRequest($request)
stuff)? Do you have this in the controller that renders the homepage? Or does it live in a different controller - e.g. a controller for the /register
page? If the registration-handling code lives at /register
, then what you need to do is make sure that your form action
attribute points to /register. In other words, right now, when you submit, it is submitting back to your homepage, and is never hitting your form logic. If this is your problem, add this to your form:
{{ form_start(form, { 'action': path('security_registration' }) }}
Change security_registration
to your route name.
If this is not your problem, let me know!
Cheers!
Thank you again for your time :)
So I put back my two routes to "/login" and "/register". Neither Authentication nor Registration work on home page. But if I use Ajax with $('form[name="login"].serialize()) and send it to the routes, both work and it's fine for me. I just have to handle success now.
Awesome content as usual. As a follow-up to this course, I created an editAction in my controller to allow my users to modify their profile, but here's the thing: if the form validation fails once and then in the following step goes through, the profile is updated but the user gets logged out. I fixed it by doing this http://stackoverflow.com/qu... (so basically by adding $em->refresh($user) if the form is not valid) but I don't really understand what's going on. Hope it's going to help someone!
Hey Giorgio Pagnoni !
Ah, fascinating! I've never run into this and I don't see the refreshUser()
in FOSUserBundle anymore (though arguablly, it should be there still: https://github.com/FriendsOfSymfony/FOSUserBundle/pull/1316).
But, it does make sense. One of the side effects of binding your User object to your profile form is that when you submit and have a validation error, your User object has been modified. Those modifications haven't been saved to the database, but as that one page builds, your security system uses the modified User object (e.g. if you update the username and you print the username on the page - like in the header - you'll temporarily see the new, updated username in that space).
About it logging you out, I can't repeat it locally right now, but I suspect I know the cause. At the end of each request, your User object is serialized into the session. Then, at the beginning of the next request, it's unserialized (and then reloaded from the database, to make sure it's fresh). When you submit the page with a validation error, it actually serializes the updated/invalid User to the session. In practice, that's not a problem because when the User object is deserialized, Symfony uses the "id" from that User to query for a fresh User (so even if some of the info on the serialized User object is "wrong", a fresh User is queried for anyways). However, when it does this process, it compares the serialized object with the fresh object in the database. If certain things have changed (e.g. the username or password), then the User is said to have "changed" and you're logged out for security reasons. I suspect that this is happening in your case. By default (unless your User implements EquatableInterface), if your username, password or salt fields changed, then your user looks like it "changed" (if you User implements AdvancedUserInterface, which I don't typically recommend, then a few other things are checked).
By calling $em->refresh()
if the form is not valid, you're reloading the User from the database so that the fresh, unedited User is the one that's serialized in the Session. In practice, this is probably a good idea - if it doesn't mess anything up in your form, I'd stick with it :).
Cheers!
Do you have a video on how to send an email after registration with a link to confirm their email address?
Hey @disqus_8fInPIBX0k
I'm afraid that we do not have such a video, but that's easy to do if you are using FOSUserBundle. Is that your case?
Cheers!
Thanks Diego, no I don't have FOSUserBundle installed as I followed this tutorial. Is it easy to add this functionality?
It requires a bit of work but is not that complex. You will need basically this:
- Generate a unique token for every new user and store it (on the user entity)
- Send an email (right before redirect) to the user containing the URL where you will verify that token
- A new endpoint (controller's action) with a token parameter, so you can do the match between user and token. If it's a match, then you will activate the user and reset the token (am I missing something else?)
I think that's it! Anyways, you can check how FOSUserBundle does it whenever you feel that you are getting lost
Cheers!
Thanks Diego, that's really helpful.
So I created a controller action to verify the token sent to the user:
/**
* @Route("/register/email-confirmation/{token}", name="email_confirmation")
*/
public function confirmEmail(Request $request, String $token)
{
$entityManager = $this->getDoctrine()->getManager();
$user = $entityManager->getRepository(User::class)->findOneBy(['confirmationToken' => $token]);
if (!$user) {
throw $this->createNotFoundException(
'No user found for token '.$token
);
}
$user->setIsActive(true);
$entityManager->flush();
$this->addFlash(
'success',
'Your email address has been activated '.$user->getFirstName().'!'
);
return $this->get('security.authentication.guard_handler')
->authenticateUserAndHandleSuccess(
$user,
$request,
$this->get('app.security.login_form_authenticator'),
'main'
);
}
However I am getting an error when trying to login the user, like in the video above.
The "security.authentication.guard_handler" service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the container directly and use dependency injection instead.
Oh, you are in Symfony4 right? Fetching services from the container has been deprecated so you have to use dependency injection. You can inject dependencies into your controller's action by adding arguments (and of course, type-hinting them). At the moment I don't remember the exact name of the service that you need but you can find it by running
bin/console debug:container```
// 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
}
}
Hi ,
I have completed the below courses :
I would like to know what is the level of those courses (beginner, intermediate or advanced ), thanks :)