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.
Related
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.
I am attempting to introduce "ghosting" into my application - wherein I can access our app from the POV of a user.
Currently using the loginUsingID function to achieve this, with a protected route only accessible by admins. However, I would also like to display to the admin that they are ghosting a user by displaying a bar across the top of our app.
I was thinking of adding a property to the user is_being_ghosted - setting it as false on logout, false on login, and true on ghostLogin.
But I realize there is a small chance an admin attempts to ghost a user, and it sets that property, and while they are investigating things within the account, the user themselves refreshes their page (they were already authenticated so do not need to login again). In that case they would see this "admin bar" across the top, which clearly I wouldn't want to happen.
Is there an efficient way to achieve what I'm trying to do here? Am I going about this the wrong way?
As jszobody has mentioned. You could rather manage the state inside the session. You secure the /ghost route and then if the original-user-id session is set you display your bar and an unghost link.
public function ghost(Request $request, $id)
{
$request->session()->put('original-user-id', Auth::user()->id);
Auth::loginUsingId($id);
return redirect('/');
}
public function unghost(Request $request)
{
if ($request->session()->has('original-user-id')) {
Auth::loginUsingId($request->session()->pull('original-user-id'));
}
return redirect('/');
}
Update:
The ghost endpoint basically accepts the id that you want to impersonate, typically found through an ajax search input or something similar. Whatever suites your use case.
Case: I'm building a forum using Laravel's Authorization as a backbone using policies. Examples of checks I run are stuff like #can('view', $forum), and #can('reply', $topic), Gate::allows('create_topic', $forum) etc. These checks basically checks if the users role has that permission for the specific forum, topic or post. That way I can give roles very specific permissions for each forum in my application.
The issue is that all of these checks go through the Gate class, specifically a method called raw() which in its first line does this:
if (! $user = $this->resolveUser()) {
return false;
}
This presents an issue when dealing with forums. Guests to my application should also be allowed to view my forum, however as you can see from the code above, Laravels Gate class automatically returns false if the user is not logged in.
I need to be able to trigger my policies even if there is no user. Say in my ForumPolicy#view method, I do if(User::guest() && $forum->hasGuestViewAccess()) { return true }
But as you can see, this method will never trigger.
Is there a way for me to still use Laravel's authorization feature with guest users?
I'm not aware of a super natural way to accomplish this, but if it's necessary, you could change the gate's user resolver, which is responsible for finding users (by default it reads from Auth::user()).
Since the resolver is protected and has no setters, you'll need to modify it on creation. The gate is instantiated in Laravel's AuthServiceProvider. You can extend this class and replace the reference to it in the app.providers config with your subclass.
It's going to be up to you what kind of guest object to return (as long as it's truthy), but I'd probably use something like an empty User model:
protected function registerAccessGate()
{
$this->app->singleton(GateContract::class, function ($app) {
return new Gate($app, function () use ($app) {
$user = $app['auth']->user();
if ($user) {
return $user;
}
return new \App\User;
});
});
}
You could go a step further and set a special property on it like $user->isGuest, or even define a special guest class or constant.
Alternatively you could adjust your process at the Auth level so that all logged-out sessions are wrapped in a call to Auth::setUser($guestUserObject).
I just released a package that allows permission logic to be applied to guest users. It slightly modifies Laravel's Authorization to return a Guest object instead of null when no user is resolved. Also every authorization check now makes it to the Gate instead of failing authorization instantly because there isn't an authenticated user.
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.
I have 3 controllers, Tokens, Stores and Users.
Token is related to the two other models, for each token there is a owner-type and owner-id.
There is also a function in both User_controller and Store_controller, called EmailTokenToUser which send the activation link to the person that registered a user or a store.
My question is: should i pass the function to the token?
if so, how should i call it? (requestAction is a bad idea, creating an object just for one function..)
any ideas?
To reuse code you should pass it to token's MODEL not the controller. That is the way it should be not the only way. To use it you should NOT use requestAction even if is inside the controller (again it is the way it should be you CAN do it with requestAction). You can do this in two ways.
App:import('model', Token');
Token::myFunction($args);
OR
$token = ClassRegistry:init('Token');
$token->myFunction($args);
OR (if you are colling it from inside a controller you may use also)
$this->loadModel('Token');
$this->Token->myFunction($args);
This is if you put it in the Token model the function. If not and go with the controller way you should do it like this
App:import('controller', 'Tokens');
TokensController::myFunction($args);
OR
App:import('controller', 'Tokens');
$token = new TokensController();
$token->myFunction($args);
Hope it helps you :)