Symfony2: how to log user out manually in controller? - symfony

i would like to do something like that in controller to log user out:
$user = $this->get('security.context')->getToken()->getUser();
$user->logOut();

Logout in Symfony2 is handled by so called logout handler which is just a lister that is executed when URL match pattern from security configuration, ie. if URL is let's say /logout then this listener is executed. There are two build-in logout handlers:
CookieClearingLogoutHandler which simply clears all cookies.
SessionLogoutHandler which invalidates the session
All you have to do is the very same the last one does. You can achieve it by simply calling:
Legacy Symfony
$this->get('security.context')->setToken(null);
$this->get('request')->getSession()->invalidate();
Symfony 2.6
$this->get('security.token_storage')->setToken(null);
$this->get('request')->getSession()->invalidate();
Warning
This will only work when remember me functionality is disabled. In other case, user will be logged in back again by means of a remember me cookie with the next request.
Please consider the extended solution if you are using remember me functionality: https://stackoverflow.com/a/28828377/1056679

Invalidating the user's session might cause some unwanted results. Symfony's firewall has a listener that always checks and refreshes the user's token. You could just do a redirect to the default logout route that you have specified in your firewall.yml (or security.yaml)
In Controller you can do this:
$this->redirect($this->generateUrl('your_logout_url'));
If you don't know the name of the logout route (your_logout_url), you can get it from the Symfony console by using this command:
app/console router:match /logout
Or newer Symfony versions:
bin/console router:match /logout
:)

We have to set user as an anonymous user when logging out. Then we can use
$token->getUser()->getRoles(); in controller or {% if is_granted('ROLE_USER') %} in the twig template.
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
...
//$providerKey = $this->container->getParameter('fos_user.firewall_name');
$token = new AnonymousToken($providerKey, 'anon.');
$this->get('security.context')->setToken($token);
$this->get('request')->getSession()->invalidate();

If rememberme functionality is enabled for your site you should also clean rememberme cookie:
$this->get('security.context')->setToken(null);
$this->get('request')->getSession()->invalidate();
$response = new RedirectResponse($this->generateUrl('dn_send_me_the_bundle_confirm', array(
'token' => $token
)));
// Clearing the cookies.
$cookieNames = [
$this->container->getParameter('session.name'),
$this->container->getParameter('session.remember_me.name'),
];
foreach ($cookieNames as $cookieName) {
$response->headers->clearCookie($cookieName);
}

In Symfony 4/5 this is just enough to remove user:
/**
* #var TokenStorageInterface $token_storage
*/
private TokenStorageInterface $token_storage;
/**
* Will force logout from system
*/
public function logoutCurrentlyLoggedInUser()
{
$this->token_storage->setToken(null);
}
Now You can create a method to use it later to check if user is logged in:
class Application extends AbstractController {...
/**
* Returns currently logged in user
* #return object|UserInterface|null
*/
public function getCurrentlyLoggedInUser()
{
return $this->getUser();
}

In case you are using symfony 4.x (I haven't tested other versions, so it still might work), you may want to use the internal logout handler of symfony (highly recommended, as it will take care of everything for you in a clean way, cookies and all). You don't need to write too much code for that either, you can simply emulate a logout request:
... // Some code, that leads you to force logout the user
// Emulating logout request
$logoutPath = $this->container->get('router')->generate('app_logout');
$logoutRequest = Request::create($logoutPath);
$logoutResponse = $this->container->get('http_kernel')->handle($logoutRequest);
// User is logged out now
... // Stuff to do after logging out, eg returning response
This will make symfony do the request response flow, thus it will call the logout handler internally. This method allows you to proceed to further custom code. Otherwise, if you invoked only the logout listener here, you would have to return the usual logout response, that now is in $logoutResponse. Optionally, if you want to return it, you would also simply:
return $logoutResponse;

The proposed solutions didn't work for me in Symfony 5.3.
It should be something as basic as
session_start();
session_destroy();
So I did this way:
$this->get('session')->start();
$this->get('session')->invalidate();
This will terminate the PHP Session, which is the way most of sessions work in Symfony.

Related

Login and set remember_me cookie after user registration in Symfony

I have a controller action which processes user's data, registers the user, logs them in and redirects them to another controller's action. However I am unable to get the "remember_me" cookie set.
On a successful registration, the controller will obtain the $user object and then pass this onto the authenticateUserAndHandleSuccess() method of the GuardAuthenticatorHandler which is part Symfony's Security Bundle.
use Symfony\Component\HttpFoundation\Request;
use App\Security\LoginFormAuthenticator;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
public function register(Request $request, LoginFormAuthenticator $authenticator, GuardAuthenticatorHandler $guardHandler) {
// do something... and get $user object
return $guardHandler->authenticateUserAndHandleSuccess(
$user,
$request,
$authenticator,
'main'
);
}
What this does is to automatically login the user. However, it does not set the remember_me cookie which is presently working and used in the login form.
From the security.yaml
remember_me:
secret: '%kernel.secret%'
lifetime: 2592000 # 1 month in seconds
path: /
remember_me_parameter: login_form[_remember_me]
How could the controller be changed to also set the "remember_me" cookie?
I don't know if this is a good idea to set remember_me during registration but if you really need it just check onLoginSuccess method of TokenBasedRememberMeServices which is executed during login process when proper remember me parameter is being send - it's just about setting proper cookie.

Symfony2 phpunit functional test custom user authentication fails after redirect (session related)

Intro: Custom user implementation to be able to use and Wordpress users:
In our project, we have implemented a custom user provider (for Wordpress users - implements UserProviderInterface) with corresponding custom user (WordpressUser implements UserInterface, EquatableInterface). I have setup a firewall in the security.yml and implemented several voters.
# app/config/security.yml
security:
providers:
wordpress:
id: my_wordpress_user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
default:
anonymous: ~
http_basic: ~
form_login:
login_path: /account
Functional phpunit testing:
So far so good - but now the tricky part: mocking authenticated (Wordpress) users in functional phpunit tests. I have succeeded mocking the WordpressUserProvider so a mocked WordpressUser will be returned on loadUserByUsername(..). In our BaseTestCase (extends WebTestCase) the mocked WordpressUser gets authenticated and the token is stored to session.
//in: class BaseTestCase extends WebTestCase
/**
* Login Wordpress user
* #param WordpressUser $wpUser
*/
private function _logIn(WordpressUser $wpUser)
{
$session = self::get('session');
$firewall = 'default';
$token = new UsernamePasswordToken($wpUser, $wpUser->getPassword(), $firewall, $wpUser->getRoles());
$session->set('_security_' . $firewall, serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
self::$_client->getCookieJar()->set($cookie);
}
The problem: losing session data on new request:
The simple tests succeed on the authentication part. Until tests with a redirect. The user is only authenticated one request, and 'forgotten' after a redirect. This is because the Symfony2 test client will shutdown() and boot() the kernel on each request, and in this way, the session gets lost.
Workarounds/solutions:
In a solution provided in question 12680675 only user ID should be used for the UsernamePasswordToken(..) to solve this. Our project needs the full user object.
In the solution provided in Unable to simulate HTTP authentication in functional test the basic HTTP authentication is used. In this case the full user object - including roles - cannot be used.
As suggested by Isolation of tests in Symfony2 you can persist instances by overriding the doRequest() method in the test client. As suggested I have created a custom test client and made an override on the doRequest() method.
Custom test client to 'store' session data between requests:
namespace NS\MyBundle\Tests;
use Symfony\Bundle\FrameworkBundle\Client as BaseClient;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Class Client
* Overrides and extends the default Test Client
* #package NS\MyBundle\Tests
*/
class Client extends BaseClient
{
static protected $session;
protected $requested = false;
/**
* {#inheritdoc}
*
* #param Request $request A Request instance
*
* #return Response A Response instance
*/
protected function doRequest($request)
{
if ($this->requested) {
$this->kernel->shutdown();
$this->kernel->boot();
}
$this->injectSession();
$this->requested = true;
return $this->kernel->handle($request);
}
/**
* Inject existing session for request
*/
protected function injectSession()
{
if (null === self::$session) {
self::$session = $this->getContainer()->get('session');
} else {
$this->getContainer()->set('session', self::$session);
}
}
}
Without the if statement holding the shutdown() and boot() calls, this method is working more or less. There are some weird problems where $_SERVER index keys cannot be found so I would like to properly re-instantiate the kernel container for other aspects of the system. While keeping the if statement, users cannot be authenticated, though the session data is the same before and during/after the request (checked by var_export to log).
Question(s):
What am I missing in this this approach that causes the authentication to fail? Is the authentication (and session check) done directly on/after kernel boot() or am I missing something else? Does anyone has another/better solution to keep the session intact so users will be authenticated in functional tests? Thank you in advance for your answer.
--EDIT--
In addition: the session storage for the test environment is set to session.storage.mock_file. In this way, the session should already be persisted between requests as describe by Symfony2 components here. When checked in the test after a (second) request, the session seems to be intact (but somehow ignored by the authentication layer?).
# app/config/config_test.yml
# ..
framework:
test: ~
session:
storage_id: session.storage.mock_file
profiler:
collect: false
web_profiler:
toolbar: false
intercept_redirects: false
# ..
My assumptions were close; it was not the session that was not persisted, the problem was in the case that mocked services are 'erased' by the kernel at a fresh request. This is the basic behaviour of functional phpunit testing...
I found out that this had to be the problem while debugging in the Symfony\Component\Security\Http\Firewall\AccessListener. There the token was found, and the (not anymore) mocked custom WordpressUser was there - empty. This explains why setting the username only instead of user object worked in the suggested workarounds stated above (no need of the mocked User class).
Solution
First of all, you don't need to override the Client as suggested in my question above. To be able to persist your mocked classes, you will have to extend the AppKernel and make some sort of kernel-modifier override with a closure as parameter. There is an explanation here on LyRiXx Blog. After injecting with a closure, you could restore the service mock after a request.
// /app/AppTestKernel.php
/**
* Extend the kernel so a service mock can be restored into the container
* after a request.
*/
require_once __DIR__.'/AppKernel.php';
class AppTestKernel extends AppKernel
{
private $kernelModifier = null;
public function boot()
{
parent::boot();
if ($kernelModifier = $this->kernelModifier) {
$kernelModifier($this);
};
}
/**
* Inject with closure
* Next request will restore the injected services
*
* #param callable $kernelModifier
*/
public function setKernelModifier(\Closure $kernelModifier)
{
$this->kernelModifier = $kernelModifier;
}
}
Usage (in your functional test):
$mock = $this->getMockBuilder(..);
..
static::$kernel->setKernelModifier(function($kernel) use ($mock) {
$kernel->getContainer()->set('bundle_service_name', $mock);
});
I still have to tweak the class and extended WebTestCase class, but this seems to work for me. I hope I can point someone else in the right(?) direction with this answer.

Symfony 2: how to have the firewall custom redirect upon custom event

my symfony app deals with multiple "blogs" (multisite wordpress style)
I have some special status for my blogs among which : closed_status.
I created a Voter for some special rights and that allows me to know if the voter should allow access or not upon verification that the blog we're accessing is open/closed...
** example url : http://blog01.myapp.com http://blog18.myapp.com ....
For now i throw a 403 error and use custom management of 403 exception but i'd like to avoid that and rather redirect at the momment of the voter processing my request.
** like what happens when you're not authenticated and you're being redirected to /login or such.
I'm not asking for a full solution, could you just tell me what classes/concepts i should be looking into ?
I thought or listeners+entryPoints thingy but don't really get how it works.
It seems to me that this needAuthentication ==> redirect to login thing is very spécifically coded in symfony.
ps: I'm using fosUserBundle and tried to override as little as i could withing the bundle and concerning hte firewall too.
The flow i'd like to achieve:
request to a blog url
=> myCustomVoter denies and dispatch(CLOSED) or dispatch(PRIVATE) or dispatch(VIPONLY)
=> and it results in a clean redirection to my setpsController actions (defined somewhere)
Dispatch a custom event in a voter. In the event listener for that event set response to RedirectResponse
Ok,
I noticed (think) that i can't add a listener to the current firewall (tell me if I'm wrong).
I also noticed that to do what i want i need to setResponse to a GetResponseEvent.
So I just created a generic listener, not attached to the firewall:
It listens to my custom event and the kernel.request that is a GetResponseEvent
For now I'm just hopping all will be listened in the order that suits me:
my service determinig if the blog is open, public... and that will eventually dispatch my custom event
my custom listener that will receive the custom event and prepare a response
my custom listener that receive the kernel.request and inject my calculated response if it exists
For my first tests it seems to work, but i need to test extensively to be sure that the firewall listeners don't mess with my process (it should not i think)
Here is how i defined my listener.
route_access_listener:
class: [...]\RouteAccessListener
arguments:
- #service_container
tags:
- { name: kernel.event_listener, event: routeAccessRedirect, method: onCustomRouteAccess }
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
Here is the implementation of my listener:
[...]
public function onKernelRequest(GetResponseEvent $event)
{
if($this->response)
$event->setResponse($this->response);
}
public function onCustomRouteAccess(RouteAccessEvent $event)
{
$type = $event->getType();
switch($type){
case RouteAccessManager::DR_IS_USER_OF_OTHER_BLOG:
$redirectPath = 'link_to_blog';
break;
case RouteAccessManager::DR_BLOG_CLOSED:
$redirectPath = 'info_closed_blog';
break;
case RouteAccessManager::DR_PRIVATE_BLOG:
$redirectPath = 'info_private_blog';
break;
case RouteAccessManager::DR_USER_NOT_ENABLED_BLOG:
$redirectPath = 'info_not_enabled_on_blog';
break;
case RouteAccessManager::DR_INVALID_HOSTNAME:
$redirectPath = 'info_invalid_hostname';
break;
}
$url = $this->container->get('router')->generate($redirectPath);
$this->response = new RedirectResponse($url);
}

Automatic user authentication

Based on this question: Automatic post-registration user authentication
$token = new UsernamePasswordToken($userEntity, null, 'main', array('ROLE_USER'));
$this->get('security.context')->setToken($token);
This worked fine on Symfony 2, but in the actual release of Symfony 2.1 this seams to do not work any more, im trying the same code but with no success (and no errors). The idea is to authenticate my user over ajax without using the login form. This because Im using this schema:
if(user_on_data_base) {
if(user_fb_linked_with_us) { // authenticate him and redirect to /home }
}
This is executed over ajax.
EDIT:
The most strange thing is that if I execute this code from my user controller it works! but executing it from my ajax controller dont.
Looking at how the HWIOAuthBundle does this, it looks like you need to throw the security.interactive_login event.
Here's how you can do it:
$this->get('event_dispatcher')->dispatch(
SecurityEvents::INTERACTIVE_LOGIN,
new InteractiveLoginEvent($this->container->get('request'), $token)
);

Symfony2 Templating without request

I'm trying to send an email from a ContainerAwareCommand in Symfony2. But I get this exception when the email template is render by:
$body = $this->templating->render($template, $data);
Exception:
("You cannot create a service ("templating.helper.assets") of an inactive scope ("request").")
I found in github that this helper need the request object. Anybody knows how can I to instance the Request object?
You need to set the container into the right scope and give it a (fake) request. In most cases this will be enough:
//before you render template add bellow code
$this->getContainer()->enterScope('request');
$this->getContainer()->set('request', new Request(), 'request');
The full story is here. If you want to know the details read this issue on github.
The problem arises because you use asset() function in your template.
By default, asset() relies on Request service to generate urls to your assets (it needs to know what is the base path to you web site or what is the domain name if you use absolute asset urls, for example).
But when you run your application from command line there is no Request.
One way to fix this it to explicitely define base urls to your assets in config.yml like this:
framework:
templating:
assets_base_urls: { http: ["http://yoursite.com"], ssl: ["http://yoursite.com"] }
It is important to define both http and ssl, because if you omit one of them asset() will still depend on Request service.
The (possible) downside is that all urls to assets will now be absolute.
Since you don't have a request, you need to call the templating service directly like this:
$this->container->get('templating')->render($template, $data);
Following BetaRide's answer put me on the right track but that wasn't sufficient. Then it was complaining: "Unable to generate a URL for the named route "" as such route does not exist."
To create a valid request I've modified it to request the root of the project like so:
$request = new Request();
$request->create('/');
$this->container->enterScope('request');
$this->container->set('request', $request, 'request');
You might need to call a different route (secured root?), root worked for me just fine.
Symfony2 Docs
Bonus addition:
I had to do so much templating/routing in cli through Symfony2 commands that I've updated the initializeContainer() method in AppKernel. It creates a route to the root of the site, sets the router context and fakes a user login:
protected function initializeContainer()
{
parent::initializeContainer();
if (PHP_SAPI == 'cli') {
$container = $this->getContainer();
/**
* Fake request to home page for cli router.
* Need to set router base url to request uri because when request object
* is created it perceives the "/portal" part as path info only, not base
* url and thus router will not include it in the generated url's.
*/
$request = Request::create($container->getParameter('domain'));
$container->enterScope('request');
$container->set('request', $request, 'request');
$context = new RequestContext();
$context->fromRequest($request);
$container->get('router')->setContext($context);
$container->get('router')->getContext()->setBaseUrl($request->getRequestUri());
/**
* Fake admin user login for cli. Try database read,
* gracefully print error message if failed and continue.
* Continue mainly for doctrine:fixture:load when db still empty.
*/
try {
$user = $container->get('fos_user.user_manager')->findUserByUsername('admin');
if ($user !== null) {
$token = $token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
$this->getContainer()->get('security.token_storage')->setToken($token);
}
} catch (\Exception $e) {
echo "Fake Admin user login failed.\n";
}
}
}
You might not need the last $container->get('router')->getContext()->setBaseUrl($request->getRequestUri()); part, but I had to do it because my site root was at domain.com/siteroot/ and the router was stripping /siteroot/ away for url generation.

Resources