Redirecting on login/logout in Symfony2 using LoginHandlers.
Using login handlers or even logout handlers, we can do a number of last minute things before the user is redirected to where they need to go. Being able to intercept the redirect at the last minute and make the user get redirected elsewhere is relatively simple.
Firstly though, i notice a lot of people now who are using Symfony 2 are cheating somewhat by using an EventListener and registering it with the security.interactive_login event listener. This is not the best way to go about this, namely because this event listener is not intended for this purpose. Sometimes event listeners are not the best way to go because they were defined for a specific purpose other than your intention and using them may have undesirable side effects you may not notice right away. Using handlers however allows a little more flexibility in that in all likelihood regardless of changes to Symfony in future revisions, they should still work and were designed specifically for the purposes we will need them for.
Firstly, we need to define our services, personally, i am a fan of YAML, so i define my services as such but feel free to do the same in XML:
parameters: ccdn_user_security.component.authentication.handler.login_failure_handler.class: CCDNUser\SecurityBundle\Component\Authentication\Handler\LoginFailureHandler ccdn_user_security.component.authentication.handler.logout_success_handler.class: CCDNUser\SecurityBundle\Component\Authentication\Handler\LogoutSuccessHandler services: ccdn_user_security.component.authentication.handler.login_failure_handler: class: %ccdn_user_security.component.authentication.handler.login_failure_handler.class% arguments: [@service_container, @router, @security.context] tags: - { name: 'monolog.logger', channel: 'security' } ccdn_user_security.component.authentication.handler.logout_success_handler: class: %ccdn_user_security.component.authentication.handler.logout_success_handler.class% arguments: [@service_container, @router] tags: - { name: 'monolog.logger', channel: 'security' }
Now our services are defined, we need to define the handlers themselves. I like to create a directory named Services in my user bundle for this purpose, then create 2 files. The first file is LoginSuccessHandler, the second is LogoutSuccessHandler. Our handlers will implement the appropriate interface for either our login or logout handlers, which require usually only one method in these instances.
Here is what the login handler should look like:
router = $router; $this->security = $security; } public function onAuthenticationSuccess(Request $request, TokenInterface $token) { if ($this->security->isGranted('ROLE_SUPER_ADMIN')) { $response = new RedirectResponse($this->router->generate('category_index')); } elseif ($this->security->isGranted('ROLE_ADMIN')) { $response = new RedirectResponse($this->router->generate('category_index')); } elseif ($this->security->isGranted('ROLE_USER')) { // redirect the user to where they were before the login process begun. $referer_url = $request->headers->get('referer'); $response = new RedirectResponse($referer_url); } return $response; } }
Note my namespace is CCDNUser, and my bundle is called SecurityBundle, so our service is named ccdn_user_security.component.authentication.handler.login_failure_handler. Change this to your own namespace and bundle name accordingly but ensure to use the same format as used here, underscore your namespace and append the bundle name with an additional underscore without the term ‘bundle’ in it. Failure to get this part correct will mean your namespace will not be loaded and you will get problems later on in this tutorial as exceptions will be thrown regarding the service not existing.
Here is what the logout handler should look like:
router = $router; } public function onLogoutSuccess(Request $request) { // redirect the user to where they were before the login process begun. $referer_url = $request->headers->get('referer'); $response = new RedirectResponse($referer_url); return $response; } }
Our service definitions inject some of the classes we need to make use of our handlers, namely a Router object and the SecurityContext object, we can store these in member variables for later use. Then we wait for the onAuthenticationSuccess method to be called in our LoginSuccessHandler, once this has happened, we have a request and token interface object at our disposal. Using the security context object we can determine what role they have (i.e; ROLE_USER, ROLE_ADMIN etc) and appropriately redirect them as needed.
I like to redirect members of ROLE_USER role to where they came from prior to logging in. This is achieved by passing the ‘referer’ header into the redirect response object we return. I took this step both in the login and logout handlers.
Last thing we now need to do, is inform our security config that it needs to use these handlers by doing the following:
# App/config/security.yml security: providers: fos_userbundle: id: fos_user.user_manager firewalls: main: #pattern: .* form_login: provider: fos_userbundle login_path: /login use_forward: false check_path: /login_check success_handler: ccdn_user_security.component.authentication.handler.login_success_handler failure_path: null logout: path: /logout target: / success_handler: ccdn_user_security.component.authentication.handler.logout_success_handler anonymous: true
In this instance i am using FOSUserBundle and extending it in my own UserBundle, though this code is contained in the SecurityBundle. Which is probably the best way to do this. As the handlers used here work in any bundle because they plugin to Symfony2 core and implement their login handlers, and should not be dependant on any user bundle.
You can see a complete implementation of this in my security bundle found on github
Once this is implemented your on your way to a better login/logout system. Enjoy.
Excellent! I was trying to find some information to insert a flash message when the user logout but without reimplementing the whole logout system.
Thank you for the article!
By the way, I can’t see the \ or / in your code, notice it in the “use” headers
@Silence i believe its wordpress or something stripping out the backslashes. Go to the source code on my github and then you can look under the UserBundle and then under Handlers directory. There you will see the full source code with the backslashes.
Thanks Reece! I figure out where to put the backslashes and it worked fine.
I have a problem right now with the logout success handler. All I wanted to do was to store a flash var to store a goodbye message before redirecting the user to the login page. I tried this, but it doesn’t show me the session var in the login page
Can you give me a hand, please?
public function onLogoutSuccess(Request $request)
{
/**
* @var Symfony\Component\HttpFoundation\Session $session
*/
$session = $request->getSession();
$session->set(‘alert’, array (
‘msg’ => ‘panel.logout’,
‘class’ => ‘p_right’
));
return new RedirectResponse($this->router->generate(‘login’));
}
The method is being called but the session var is missing after the redirect
@var i think this is because the session you are addressing is from the Request object, but after logout that session gets destroyed. You will likely need to create a new session in your logout handler. I have never done that before though so cannot advise. But that is what i would do, create a new session using Symfony’s Session class. Hope that helps, but if not then check out the IRC room, on irc.freenode #symfony.
good luck.
you are right. Even it doesn’t make too much sense to me, because I think that the logout success handler should be cause AFTER the symfony logout stuff…
I’m very newbie on Symfony2, I’ll have to read more about it or forget about this feature right now.
Something I know that is going to work would be to create a redirect response that redirects to a dummy logout when I can set the message and redirect to the login form after that, but too much overhead to accomplish a simple task.
Another cool thing might be to have a “just_logout” var or method in your controller. I implemented it in my own framework. The credentials logout method, logouts the user but sets a flag that I can use in my controller to set the goodbye message or whatever I want… I’m missing something like that with this symfony system
I think I found the solution. I’m using the LogoutHandlerInterface instead of the LogoutSuccessHandlerInterface.
In the security configuration you have to set the “handler” instead of the “success_handler” and in my new class I just have (see below):
It allows me to invalidate the session (which logouts the user) but I can set the flash var after that ^_^
Two days to do such a simple task, but at least I’m learning Symfony2
Regards
getSession();
$session->invalidate();
$session->setFlash(‘alert’, array (
‘msg’ => ‘panel.logout’,
‘class’ => ‘p_right’
));
}
}
@silence congrats on getting it working. There were times before when i too have spent a day or 2 working on getting something working. There are still things i am learning myself about Symfony.
Hi,
I was exactly looking for a way to do this, I just have one question:
Where does this line points to:
id: fos_user.user_manager
?
I just can’t put this code to work because of this line.
Best regards.
@Xocoatzin
That line is for the FOSUserBundle, which you can download from github. It is a user bundle that handles user registration, login, account changes,change of password, account recovery and lastly account activation via email.
The line in question is used by FOSUserBundle to state which user_manager to use, which is a custom class in FOSUserBundle to handle the User entities various interactions, which should be mostly database related stuff.
If your not using FOSUserBundle then simply remove that line and hopefully you won’t have any issues. The configurations that are used in the app/config.yml file must all exist somewhere in Symfony or in the various bundles. They are defined in a bundles DependencyInjection folder, in the Configuration.php file. If no bundle or Symfony itself has defined a configuration option that is present in the app/config.yml then Symfony will spit out an error/exception. Or putting it another way, you can only put configurations into your app/config.yml that have been defined in the various Configuration.php files of each bundle under the tree builder.
Hope this helps and good luck.
You’re rigth, I removed that line and worked perfectly. This was a great tutorial. Thanks !!
Thanks a lot! This helped me when I wondered what to return in my login handler…
@COil Yes i am aware of that issue, its WordPress, it seems to use php’s strip slashes function i believe to prevent SQL injection. I have addressed the issue and it is fixed now. 🙂
Good tutorial, it deserves an entry in the Symfony2 documentation (or in the cookbook) 🙂
PS: Be careful, classes name are broken, the are missing \
Thank you for this, I was going quite strange trying to get a Ping SSO component working over a site that was part anonymous, part authenticated, and part allow either. This was very helpful.
Hi Reece, im fairly new to symfony and there is some things in your post that I don’t quite understand:
1) Why did you register a service called “login_failure_handler” in your first code snippet? Should not be “login_success_handler”?
2) Why did you say “I like to create a directory named Services in my user bundle for this purpose” when you are actually storing those classes inside “Component\Authentication\Handler” folder?
3) Is it neccesary to install the FOSUserBundle to register the handlers just for redirect after login?
I used to be able to find good info from your blog articles.
I realise this is an ages old blog post, but I have a quick question.
I agree with your method being the most elegant and appropriate than injecting an event listener. However, I don’t understand why you pass the router as an argument to your handler, seems unnecessary, and the whole service container too.
Are you able to elaborate on this? Am I missing something? You don’t even seem to use the router object to generate anything either, just a redirect.
Cheers!
You are right of course. It is unnecessary to inject the container and router, however i toned down what i have for the purposes of the blog. I actually make use of the container to get parameters inside the login handlers. As for the router, i think at some point in the past i was making use of it for something, cannot remember what it was now. But it would be safe to not use it.
I will post a new blog article to redo this in a more up-to-date and cleaner version, where i will show some of the things i am doing in my own handlers and remove the router service i am injecting into the handler as it is no longer needed.
I think this article has been in need of an update for a while now. Another thing though, maybe you would like to take a look at my bundle that is making use of these handlers here for my SecurityBundle.
Hi Reece,
I have spent a bit of time looking at all the CCDN stuff on Github, my gosh you are an excellent Symfony programmer!
I have been using your code repository as a basis to learn the Symfony framework and it has been an absolutely indispensable resource for understanding how to accomplish various features while gaining an abstracted overview of how to actually put together a site cohesively from bundles. Thanks again, I’m glad I stumbled across your github!
Cheers!
Your welcome. Though there is still lots to learn for myself, and i am continually rediscovering more approaches to solving some of the things i do, aswell as coming up with my own ideas to better write SF2 code.
The code i have now works, but there are some aspects i am aware of that could be done better. Part of the issue of building a site using SF2 is trying your hardest to keep bundles decoupled while still retaining the features you want. Thats more for the benefit of other people who may wish to use them said bundles. I have been working on updating all the bundles and have already managed to decouple a lot more of the logic including numerous bug fixes. Though work continues on that effort i hope to have an update deployed on the codeconsortium site (and github) very soon (hopefully a week). Will push to master on the repos in a few hours as i go though.
🙂
Thanks for the code!!
But there is a error, you created this examples for LoginSuccess and LogoutSuccess and you have LoginFailure and LogoutSuccess in your services.yml
And for that it took me more time to implement it 🙂
Hi Reece,
I have implemented the above code, i have also changed LoginFailureHandler to LoginSuccessHandler. Everything is working fine except that when i refresh my page i get this error,
Catchable Fatal Error: Argument 1 passed to Zaintechs\Bundle\l2uUserBundle\Component\Authentication\Handler\LoginSuccessHandler::__construct() must be an instance of Symfony\Component\Routing\Router, instance of appDevDebugProjectContainer given, called in /home/launch2u_new/app/cache/dev/appDevDebugProjectContainer.php on line 175 and defined in /home/launch2u_new/src/Zaintechs/Bundle/l2uUserBundle/Component/Authentication/Handler/LoginSuccessHandler.php line 18
It says that “public function __construct(Router $router, SecurityContext $security)” is an instance of appDevDebugProjectContainer, it must be an instance of Symfony\Component\Routing\Router. I have included use Symfony\Component\Routing\Router; as well. I am new to symfony please help. I am using Symfony2.1 . Please help me,
Regards,
Shabbir
@Shabbir
Alter the services.yml file and remove the service container from the handlers definitions so that it is not injected, or add the service container to the constructor parameters.
It may be however that the interface in later versions stipulates a different set of constructor parameters in which case investigate the interface that the handler must implement.
Thankyou, its very helpful
hi
you have an error in your code example.
`__construct` of your `LoginSuccessHandler` class is expecting different arguments than set in your `services.yml`
correct code should looks like this:
`services.yml`:
“`
ccdn_user_security.component.authentication.handler.logout_success_handler:
class: %ccdn_user_security.component.authentication.handler.logout_success_handler.class%
arguments: [@security.context, @router]
tags:
– { name: ‘monolog.logger’, channel: ‘security’ }
“`
and `__construcotr` of `LoginSuccessHandler`:
“`
public function __construct(SecurityContext $security, Router $router) {…}
“`
thank you gondo you saved my life 🙂
Hi !
I try to use FosUserBundle with two user interface :
FrontEnd : /
BackEnd : /admin/
So I use LoginHandler.
I have a bundle (CulturalStore/UserBundle) which extends FosUserBundle, in it services.xml :
In LoginSuccessHandler I have :
And in LogoutSuccesHandler :
Finally, in the global security.yml I have :
f
But, when I go to the website, I have this error :
`FatalErrorException: Error: Class ‘CulturalStore\UserBundle\Component\Authentication\Handler\LoginSuccessHandler’ not found in /Users/Juju/Sites/workspace/CS/app/cache/dev/appDevDebugProjectContainer.php line 428`
Any idea how to redirect the user when not authenticated with the login form?
The api documentation says that AuthenticationSuccessHandler is called when an interactive authentication attempt succeeds. http://api.symfony.com/2.0/Symfony/Component/Security/Http/Authentication/AuthenticationSuccessHandlerInterface.html
That means the redirect happens only when user is authenticated via the login form i.e. enters username and password and submits that information. This method does not work when we directly login the user grammatically via password reset link or oauth api provider ( Facebook,Google, etc)
@Mirk, then that is the responsibility of your UserBundle.
Most people use FOSUserBundle, and fortunately they provide us with hooks via the Event Dispatcher, so you can hook into the process for password change / account recovery etc and intercept the response event and return a redirect response.
Iam using Fosuser bundle as u explained.
but my security context class always giving me ROLE-User even they are admins or super admins
how can i solve this plese help me
Hey this is very informative, here is another example of Symfony2 FosUserBundle.
http://webmuch.com/override-fosuserbundle/
I hope you like it.
Hey Reece, Thank you so much. This example is very useful. But i’m getting a “ServiceNotFoundException” with the message “The service “security.firewall.map.context.main” has a dependency on a non-existent service “(my service name)”. I’ve already declared the service and checked the names. Any idea?
Hello, this thread could help to keep this method update, as SecurityContext will be removed in symfony 3.0 and deprecated since 2.6. http://stackoverflow.com/questions/29606237/symfony-2-securitycontext-class-deprecated
thank you for your article , but I have not understand the securityBundle your own bundle or as fosuser you install , I have to create the where to install because I have already install fosuser ?