I have followed the tutorial to make may application able to logout users simply by calling a route like /logout (Via the Security module as described in the official documentation). It works.
Now I would like to logout the user (still logged via the described in the doc "Remember me" function) in my own controllers (For example before an email validation, in case another session is still opened under another account).
But none of my methods works, it makes me crazy. I have tried $session->clear(), $session->invalidate(), $request->getSession->clear(), $request->getSession->Invalidate(), etc. etc. Nothing works.
So my question are, please: How do you do it? How should I handle this case? Is it related to the "remember me" functionality (maybe it's managed in another cookie or something?) ?
Thanks in advance
Your guess might be right, that the issue could be related to the remember me functionality as this will use cookies to store the token, instead of the session, and therefore need a different LogoutHandler.
Symfony provides multiple ways to handle authentication and you will need the correct LogoutHandler(s) depending on your current settings.
Solving your issue is surprisingly hard if you don't just want to redirect the user to the logout path. The "best" way I can think of right now, is simulating a logout-request by building the Request-object manually and then dispatching a GetResponseEvent with it so, that the LogoutListener will be triggered. Dispatching the event might have weird side effects, so you might even want to trigger the listener directly. It could look something like this:
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class MyController
{
private $kernel;
private $logoutListener;
public function __construct(HttpKernelInterface $kernel, LogoutListenerInterface $logoutListener)
{
$this->kernel = $kernel;
$this->logoutListener = $logoutListener;
}
private function customLogout()
{
// This needs to be updated whenever the logout path in your security.yaml changes. Probably something you want to improve on.
$request = Request::create('/logout');
$event = new GetResponseEvent($this->kernel, $request);
$this->logoutListener->handle($event);
}
public function someAction()
{
$this->customLogout();
// Do whatever you want to do for your request
}
}
I don't think this is a good solution as interfering with the security system is inherently dangerous. Directly calling the LogoutListener and passing around the kernel are also a bit iffy. To be honest I'm not even 100% sure this will work, but this is the closest to what I could come up with as a possible solution to your problem. You might want to rethink what you are doing and find an alternative approach.
Related
I have this test:
public function test_user_can_access_the_application_page(
{
$user=[
'email'=>'user#user.com',
'password'=>'user1234',
];
$response=$this->call('POST','/login',$user);
$this->assertAuthenticated();
$response->assertStatus(302)
->assertRedirect('/dashboard')
->assertLocation('/dashboard');
$response=$this->call('GET','/application/index');
$response->assertLocation('/application/index');
}
After I log in, it directs me to the dashboard ok until now, but if I want to access the other page after that, I cant. This error comes up.
Expected :'http://mock.test/application/index'
Actual :'http://mock.test'
Aren't multiple calls allowed in the same test, or is another way to access other pages after login?
(Note: It's not possible to use factories for the actingAs so I need to login).
If you can't use factories for actingAs, then you should try with cookie.
Look at the https://github.com/firebase/php-jwt library.
I guess you will need to call the function as an user, since you can only access it logged in. Laravel provides the actingAs() method for such cases.
https://laravel.com/docs/7.x/http-tests#session-and-authentication
You can create a random User who has the permission to log into your app or take a seeded one and call the function acting as the chosen User.
$response=$this->actingAs($user)->call('GET','/application/index');
If you call it without actingAs(), your middleware will redirect you back to the login or home screen (what you defined in the LoginController ).
In my opinion this test case should have its own testing method. I recommend using a test method per route or per use case. It makes your tests clearly arranged and easy to understand.
If you want to be authenticated, the easiest way is to have PHPUnit simulate authentication using the actingAs() method.
This method makes the user authenticated, so you wouldn't want to test the login method with it. You should write your login tests separate from testing the other pages.
To answer your question, yes you can make multiple requests in the same test, but in this case linking the login test to the 'application/index' page likely does not make much sense.
public function test_the_user_can_login()
{
$user = [
'email'=>'user#user.com',
'password'=>'user1234',
];
$response = $this->call('POST','/login',$user);
$this->assertAuthenticated();
$response->assertStatus(302)
->assertRedirect('/dashboard')
->assertLocation('/dashboard');
}
public function test_user_can_access_the_application_page()
{
$user = User::where($email, "user#user.com")->first();
$response = $this->actingAs($user)
->call('GET','/application/index');
$response->assertLocation('/application/index');
}
I experienced that in laravel 8 I use comment #test and involved second test!!! I mean if you use two function for test you must us #test that php artisan test, test both of them.
TL;DR: how can you add custom constraints (i.e. security voters) to transitions?
My application needs some workflow management system, so I'd like to try Symfony's new Workflow Component. Let's take a Pull Request workflow as an example.
In this example, only states and their transitions are describes. But what if I want to add other constraints to this workflow? I can image some constraints:
Only admins can accept Pull Request
Users can only reopen their own Pull Request
Users can not reopen PR's older than 1 year
While you can use Events in this case, I don't think that's the best way to handle it, because an event is fired after $workflow->apply(). I want to know beforehand if a user is allowed to change the state, so I can hide or disable the button. (not like this).
The LexikWorkflowBundle solved this problem partially, by adding roles to the steps (transitions). Switching to this bundle might be a good idea, but I'd like to figure out how I can solve this problem without.
What is the best way to add custom entity constraints ('PR older than 1 year can't be reopened') and security constraints ('only admins can accept PR's', maybe by using Symfony's Security Voters) to transitions?
Update:
To clarify: I want to add permission control to my workflow, but that doesn't necessarily mean I want to tightly couple it to the Workflow Component. I'd like to stick to good practices, so the given solution should respect the single responsibility principle.
The best way I found was implementing the AuthorizationChecker in the Workflow's GuardListener.
The demo application gives a good example:
namespace Acme\DemoBundle\Entity\Listener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\Workflow\Event\GuardEvent;
class GuardListener implements EventSubscriberInterface
{
public function __construct(AuthorizationCheckerInterface $checker)
{
$this->checker = $checker;
}
public function onTransition(GuardEvent $event)
{
// For all action, user should be logger
if (!$this->checker->isGranted('IS_AUTHENTICATED_FULLY')) {
$event->setBlocked(true);
}
}
public function onTransitionJournalist(GuardEvent $event)
{
if (!$this->checker->isGranted('ROLE_JOURNALIST')) {
$event->setBlocked(true);
}
}
public function onTransitionSpellChecker(GuardEvent $event)
{
if (!$this->checker->isGranted('ROLE_SPELLCHECKER')) {
$event->setBlocked(true);
}
}
public static function getSubscribedEvents()
{
return [
'workflow.article.guard' => 'onTransition',
'workflow.article.guard.journalist_approval' => 'onTransitionJournalist',
'workflow.article.guard.spellchecker_approval' => 'onTransitionSpellChecker',
];
}
Using Symfony, I am displaying a table with some entries the user is able to select from. There is a little more complexity as this might include calling some further actions e. g. for filtering the table entries, sorting by different criteria, etc.
I have implemented the whole thing in an own bundle, let's say ChoiceTableBundle (with ChoiceTableController). Now I would like to be able to use this bundle from other bundles, sometimes with some more parametrization.
My desired workflow would then look like this:
User is currently working with Bundle OtherBundle and triggers chooseAction.
chooseAction forwards to ChoiceTableController (resp. its default entry action).
Within ChoiceTableBundle, the user is able to navigate, filter, sort, ... using the actions and routing supplied by this bundle.
When the user has made his choice, he triggers another action (like choiceFinishedAction) and the control flow returns to OtherBundle, handing over the results of the users choice.
Based on these results, OtherBundle can then continue working.
Additionally, OtherOtherBundle (and some more...) should also be able to use this workflow, possibly passing some configuration values to ChoiceTableBundle to make it behave a little different.
I have read about the "Controller as Service" pattern of Symfony 2 and IMHO it's the right approach here (if not, please tell me ;)). So I would make a service out of ChoiceTableController and use it from the other bundles. Anyway, with the workflow above in mind, I don't see a "good" way to achieve this:
How can I pass over configuration parameters to ChoiceTableBundle (resp. ChoiceTableController), if neccessary?
How can ChoiceTableBundle know from where it was called?
How can I return the results to this calling bundle?
Basic approaches could be to store the values in the session or to create an intermediate object being passed. Both do not seem particularly elegant to me. Can you please give me a shove in the right direction? Many thanks in advance!
The main question is if you really need to call your filtering / searching logic as a controller action. Do you really need to make a request?
I would say it could be also doable just by passing all the required data to a service you define.
This service you should create from the guts of your ChoiceTableBundleand let both you ChoiceTableBundle and your OtherBundle to use the extracted service.
service / library way
// register it in your service container
class FilteredDataProvider
{
/**
* #return customObjectInterface or scallar or whatever you like
*/
public function doFiltering($searchString, $order)
{
return $this->filterAndReturnData($searchString, $order)
}
}
...
class OtherBundleController extends Controller {
public function showStuffAction() {
$result = $this->container->get('filter_data_provider')
->doFiltering('text', 'ascending')
}
}
controller way
The whole thing can be accomplished with the same approach as lipp/imagine bundle uses.
Have a controller as service and call/send all the required information to that controller when you need some results, you can also send whole request.
class MyController extends Controller
{
public function indexAction()
{
// RedirectResponse object
$responeFromYourSearchFilterAction = $this->container
->get('my_search_filter_controller')
->filterSearchAction(
$this->request, // http request
'parameter1' // like search string
'parameterX' // like sorting direction
);
// do something with the response
// ..
}
}
A separate service class would be much more flexible. Also if you need other parameters or Request object you can always provide it.
Info how to declare controller as service is here:
http://symfony.com/doc/current/cookbook/controller/service.html
How liip uses it:
https://github.com/liip/LiipImagineBundle#using-the-controller-as-a-service
I started to explore the world of Symfony 2 now and face with some realy strange problems i would not think they can occure in such a professional framework. I will show you the problems i face one by one:
1) How to get the recent actionName?
I found only this solution which is imho semiprofessional:
$request->attributes->get('_controller');
// will get yourBundle\Controller\yourController::CreateAction
$params = explode('::',$request->attributes->get('_controller'));
// $params[1] = 'createAction';
$actionName = substr($params[1],0,-6);
Is this serious, i have to do some extra-work to get it, why.. Is there a better solution? Creating a base controller class with a method e.g. getActionName(), but why do i have to implement such basic functionality in a framework. Is there a other way?
2) When i forward a request the code in 1) will not work.
$request = $this->container->get('request');
$getParameterList = $request->query->all();
if (!empty($getParameterList['mode'])
&& $getParameterList['mode'] == 1) {
return $this->forward('AcmeDemoBundle:Routing:lawyersearch', array(), $getParameterList);
}
The reason why it will not work is that "AcmeDemoBundle:Routing:lawyersearch" is a other format than when i came directly from a route. Second problem here is that i have to forward the GET-paramters as well(i think POST too). Is there a way that i do not have to care about it?
3) How to use a default template without using this annotation:
/**
* #Template()
*/
public function indexAction()
{
return array();
}
I do not want to have above all my methods this annotation; i know i can put it on the top of the class definition. Is there a way to achieve this? The only solution i see, is to write a BaseController that determines by a method out of the module/controller/action the default template.
4) I found classes that use public attributes e.g. Symfony\Component\Validator\Constraints\Length with e.g. public $max;
How to solve this? Very strange because this is not professional to use public attributes.
I hope someone has easy solutions for this. It would be realy dissapointing if Symfony 2 has so much strange behaviour in so much cases. 4 strange things i 2 days since i began to explore it. It gives me the feeling that there is much more when i continue.
Please confirm that there are no other solution by the framework or which is the solution. Thank you
1) By accessing the '_controller' parameter of the request, you are delving into the internals of Symfony2. They rarely document anything related to this outside of routing. You should use controller actions more definitively, don't try to automate too much on this level.
2) Symfony2 can't account for highly dynamic controllers. You know it is possible to call ->forward more than once, and within the same controller action. This creates a nesting nightmare that the Symfony developers weren't prepared to deal with.
This is one of the reasons $request = $this->container->get('request'); is now deprecated in favour of $stack = $this->container->get('request_stack');. Because forwarding needs to create new internal requests.
3) Also deprecated. Symfony2 best practices now discourages the use of #Template() with empty parameters because of the potentially volatile development of actions/templates. You are supposed to explicitly define which template to use, if you use one at all. This comes in handy when dealing with data-only responses. You wouldn't want your responses to use a template automatically as this would result in unexpected behaviour in your design.
1) Use Constant: __FUNCTION__
http://php.net/manual/en/language.constants.predefined.php
2) Try setMethod on $request:
$this->get('request')->setMethod('POST');
3) I do not know, probably not possible.
4) Symfony\Component\Validator\Constraints\Length is one of constraints:
http://symfony.com/doc/current/book/validation.html#constraints
I am building an API in CakePHP. I have a function that as part of its execution first destroys the cookies associated with the session. I am using the following code to do this.
public function new_thing () {
// I first call another controller to use functions from that controller
App::import('Controller', 'Person');
$PersonsController = new PersonsController;
// This function call is the problem
// This does not throw any errors but does not destroy the cookie as requested
$PersonsController->_kill_auth_cookie()
}
// This is from the Person controller, these are the functions used in the API
// This is the function that sets the cookies
public function _set_auth_cookie( $email ) {
setcookie(Configure::read('auth_cookie_name'), $email);
}
// this is the function that does not properly destroy the cookie from the API
// interestingly, it does work when called from in this controller
public function _kill_auth_cookie() {
setcookie(Configure::read('auth_cookie_name'), 'xxx', time()-7200);
}
I cannot get the API to properly expire the cookie that is created earlier in the session, I am not sure why. Additionally—what is maddening—is that the logs are empty and no error is being thrown of any kind, so I am not sure what to do next.
There is so much wrong in this code and concept…
DON'T instantiate controllers anywhere. It is plain wrong, broken by design and violates the MVC pattern. Only one controller should be dispatched by the framework itself based on the request; you don’t instantiate them manually.
An API using cookies? Well, not impossible but definitely not nice to work with. It’s possible but I’ve never seen one in the wild. I feel sorry for the person who has to implement it. See this question.
Why are you not using the CookieComponent? It has a built-in destroy() method to remove a cookie.
If you have an “auth” cookie, why are you not using CakePHP’s built-in Auth system? It will deal with all of that.
Use App::uses() not App::import() here
By convention, only protected functions should be prefixed with _
The first point is very likely the reason why cookie and sessions are messed up because the second controller instance initiates components again, and by this cookie and session maybe a second time as well. However, this can lead to “interesting” side effects.
I first call another controller to use functions from that controller
This is the evidence that your architecture is broken by design. The code that needs to be executed somewhere else; should be in a model method in this case. Or at least a component if there are controller-related things to be shared between different controllers.