I'm writing a Feature test for an API and I want to test custom auth logic.
I know that when I call the login API endpoint, Laravel caches the fact that the user is logged in so the next API call in the test would consider the user already authenticated...
So for one test, how do I disable this Laravel magic of auth caching so I can manually provide the Bearer auth token to check if my custom authentication logic works?
I'm thinking something along the lines of clearing the guards in AuthManager, or clearing AuthManager entirely so Laravel would be force to reinitialize it. But I'm having no luck in figuring out how to do that in a way that works.
Here's some pseudo-example code:
public function testLogin()
{
$responseData = $this->post('/login', ['email' => $this->user->email, 'password' => 'password'])->json();
$this->get('/endpoint-requiring-auth')->assertOk();
//
// $this->logicToResetAuth() ?
//
$this->get('/endpoint-requiring-auth')->assertUnauthorized();
// I want to manually have to pass the token to get it to work now.
$this->get('/endpoint-requiring-auth', ['Authorization' => "Bearer $responseData[access_token]"])
->assertOk();
}
Also the unknown reset logic should affect multiple guards.
Oh yeah, I'm writing custom logic around the JWT-Auth library.
For Laravel 8.x (and maybe newer)
auth()->forgetGuards();
But for JWT you may need to additionally do:
app('tymon.jwt')->unsetToken();
Or
app()->instance('tymon.jwt', null);
For Laravel 7.x (and maybe older)
As ->forgetGuards() is not invented yet in this version, and guards-array is protected, do something like:
/** #var \Illuminate\Auth\AuthManager $auth */
$auth = app('auth');
$mirror = new \ReflectionObject( $auth );
$prop = $mirror->getProperty( 'guards' );
$prop->setAccessible(true);
$prop->setValue($auth, []);
$prop->setAccessible(false); // Back to protected.
But for JWT, do same as above Laravel 8 section.
For jwt-auth, you can do this to clear the auth user:
auth()->forgetGuards();
app()->instance('tymon.jwt', null);
The first line, discards the existing cached guards. And the second, ensures when the guard is re-created, it wont reuse the same underlying jwt instance.
Good question. I was also struggling with this too. Yesterday I performed a deep dive on this subject. Turned out that TokenGuard has en implementation problem as the authenticated user is not refreshed after a new request is loaded. In order to fix this the following is needed:
Extends TokenGuard class into your own class and overload method setRequest. Add '$this->user = null;' before calling the parent implementation by 'return parent::setRequest($request);'
Use Auth::extend to be able to use your custom TokenGuard
After that, every new request resets the authenticated user automatically
See also https://code.tutsplus.com/tutorials/how-to-create-a-custom-authentication-guard-in-laravel--cms-29667
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.
I'm writing unit tests for an API using PHPUnit and Laravel. Most functions I'm testing require that the user is authenticated before the function can be ran. The user data is stored in one table, and their permissions are stored inside of another table. I can fake the user object inside of Laravel, but I need to be able to also pull the corresponding permissions from the other table without having to hit the database like the dingo router currently is doing.
Currently running Laravel 5.8 and PHPUnit 8.1.5. I currently have the users object that I generated from a Laravel factory saved to a text file. I am able to pass that to a function called "actingAsApi" (found on Github, code below) and that allows me to authenticate as that user. However, the function is still going out and getting all permissions for that user from the database. I'm trying to mock or fake the permissions object it is pulling somewhere so that it doesn't need to hit the database at all. I also tried using the built in Passport functions for Passport::actingAs, and those did not work either as they were still hitting the DB (and not really working anyways).
actingAsApi (inside of TestCase.php)
protected function actingAsApi($user)
{
// mock service middleware
$auth = Mockery::mock('Dingo\Api\Http\Middleware\Auth[handle]',
[
Mockery::mock('Dingo\Api\Routing\Router'),
Mockery::mock('Dingo\Api\Auth\Auth'),
]);
$auth->shouldReceive('handle')
->andReturnUsing(function ($request, \Closure $next) {
return $next($request);
});
$this->app->instance('Dingo\Api\Http\Middleware\Auth', $auth);
$auth = Mockery::mock('Dingo\Api\Auth\Auth[user]',
[
app('Dingo\Api\Routing\Router'),
app('Illuminate\Container\Container'),
[],
]);
$auth->shouldReceive('user')
->andReturnUsing(function () use ($user) {
return $user;
});
$this->app->instance('Dingo\Api\Auth\Auth', $auth);
return $this;
}
Test inside of my Test file
public function testActAs() {
$user = 'tests/users/user1.txt';
$this->actingAsApi($user);
$request = new Request;
$t = new TestController($request);
$test = $t->index($request);
}
I expect the actingAsApi function to allow me to also pass in the mock permissions data that corresponds to my mock user object data from the file, but instead it is hitting the database to pull from the permissions table.
EDIT:
So i've been playing around with doing mock objects, and i figured out how to mock the original controller here:
$controlMock = Mockery::mock('App\Http\Controllers\Controller', [$request])->makePartial();
$controlMock->shouldReceive('userHasPermission')
->with('API_ACCESS')
->andReturn(true);
$this->app->instance('App\Http\Controllers\Controller', $controlMock);
but now I can't figure out how to get my call from the other controllers to hit the mocked controller and not a real one. Here is my code for hitting an example controller:
$info = $this->app->make('App\API\Controllers\InfoController');
print_r($info->getInfo('12345'));
How can i make the second block of code hit the mocked controller and not standup a real one like it does in its constructor method?
Finally came on an answer, and it is now fixed. Here's how I did it for those wondering:
$request = new Request;
$controlMock = m::mock('App\API\Controllers\InfoController', [$request])->makePartial();
$controlMock->shouldReceive('userHasPermission')
->with('API_ACCESS')
->andReturn(true);
print_r($controlMock->getInfo('12345'));
Basically, I was trying to Mock the original API controller, and then catch all of the calls thrown at it. Instead, I should've been mocking the controller I'm testing, in this case the InfoController. I can then catch the call 'userHasPermission', which should reach out to the Controller, but I am automatically returning true. This eliminates the need for hitting the database to receive permissions and other info. More information on how I solved it using Mockery can be found here: http://docs.mockery.io/en/latest/cookbook/big_parent_class.html. As you can see, this is referred to as a 'Big Parent Class'. Good luck!
I created a way to authenticate a user with API keys, thanks to a class A implementing the SimplePreAuthenticatorInterface interface. Everything works well (the user is successfully authenticated).
I'm trying to store the API keys, for a later use during the user's journey. To do so, inside the authenticate method of my class A, I return a PreAuthenticatedToken in which the credentials are my API keys.
The problem is : Inside a custom service, when I try to get the token credentials, I get null... I successfully get the API keys when I comment the line 76 of the PreAuthenticatedToken Symfony core class :
public function eraseCredentials()
{
parent::eraseCredentials();
//$this->credentials = null;
}
My questions are:
1) Why is the method eraseCredentials called whereas the user is authenticated? I thought this method was called during user's logging out...
2) What am I doing wrong? Is the PreAuthenticatedToken token the right place to store my API keys? How can I get them back from a custom service ?
Thanks for helping me. :)
PS : I'm a newbee on posting in Stackoverflow (and in English ^^). Sorry in advance for any mistakes.
I found another similar question but it has no helping response and I added some more precisions.
EDIT: The code of my custom service where I try to get the credentials is:
$token = $this->container->get("security.token_storage")->getToken();
if ($token !== null) {
$credentials = $token->getCredentials();
// $credentials is null here
}
EDIT 2: The return part in my code of my SimplePreAuthenticatorInterface::authenticateToken method :
return new PreAuthenticatedToken(
$user,
$apiKeys,
$providerKey,
$user->getRoles()
);
Ad 1. It depends on your AuthenticationProviderManager: this class accepts $eraseCredentials as second parameter - by default set to true (here).
That's why eraseCredentials method is being called on PreAuthenticatedToken $token during authenication (here).
Ad 2. Please check How to Authenticate Users with API Keys tutorial. You should create your custom ApiKeyAuthenticator class and add logic there.
According to your comment:
Note that authenticateMethod from tutorial is being called inside authenticate method (here). At that time token credentials are not erased yet and you can access them. For security reason they are erased after authentication (but this can also be changed / configured via security.yml file). If you need them later you can introduce custom token class and store API key there.
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 made a child class that extends from DefaultAuthenticationSuccessHandler in order to add custom feature during the authentication process. I want to stop the authentication process if the criteria is not met. How do I do that?
public function onAuthenticationSuccess( Request $request, TokenInterface $token) {
if(some_condition_applies){
//if success, resume default flow
return parent::onAuthenticationSuccess( $request, $token);
}else{
//how to fail the authentication here?
}
}
AuthenticationSuccessHandler's main purpose is to do something AFTER the user has been authenticated. In other words it is too late to do anything about it.
You can create a custom authentication provider which handles all the authentication logic you require.
http://symfony.com/doc/current/cookbook/security/custom_authentication_provider.html#the-authentication-provider
Depending on your setup, the new SimpleFormAuthenticator functionality in Symfony 2.4 may help you out: How to Create a Custom Form Password Authenticator
This is probably only applicable if your authentication mechanism is username/password based (not OAuth or something).
Additionally, you could potentially just implement the AdvancedUserInterface, and then return "false" from a method like "isEnabled" to prevent login (Forbid Inactive Users). The only disadvantage is that you only have access to data in your User class. If you need something beyond that, the above method should work (but is a little bit more work).
Cheers!