I am using Laravel 5.1 and I am trying to test my controllers.
I have several roles for my users and policies defined for different actions. Firstly, each of the requests needs to be made by an authenticated user, so running a test with no user returns a 401 Unauthorized, as expected.
But when I want to test the functionality for authorized users, I still get the 401 Unauthorized status code.
It may be worth mentioning that I use basic stateless HTTP authentication on these controllers.
I have tried the following:
public function testViewAllUsersAsAdmin()
{
$user = UserRepositoryTest::createTestAdmin();
Auth::login($user);
$response = $this->call('GET', route('users.index'));
$this->assertEquals($response->getStatusCode(), Response::HTTP_OK);
}
and
public function testViewAllUsersAsAdmin()
{
$user = UserRepositoryTest::createTestAdmin();
$response = $this->actingAs($user)
->call('GET', route('users.index'));
$this->assertEquals($response->getStatusCode(), Response::HTTP_OK);
}
and also this (in case there was anything wrong with my new user, which there shouldn't be)
public function testViewAllUsersAsAdmin()
{
$user = User::find(1);
$response = $this->actingAs($user)
->call('GET', route('users.index'));
$this->assertEquals($response->getStatusCode(), Response::HTTP_OK);
}
but in every case I get a 401 response code so my tests fail.
I can access the routes fine using postman when logging in as a dummy user.
I am running out of ideas, so any help would be appreciated.
You need to add Session::start() in the setUp function or in the beginning of the function which user need to log in.
public function setUp()
{
parent::setUp();
Session::start();
}
or
public function testViewAllUsersAsAdmin()
{
Session::start();
$user = UserRepositoryTest::createTestAdmin();
Auth::login($user);
$response = $this->call('GET', route('users.index'));
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
}
Through some experimentation, I found that the problem lay inside my authentication middleware. Since I want the API to be stateless, the authentication looks like this:
public function handle($request, Closure $next)
{
return Auth::onceBasic() ?: $next($request);
}
And apparently, it's not possible to authenticate a user the way I was doing it.
My solution was simply to disable the middleware, using the WithoutMiddleware trait or $this->withoutMiddleware() at the beginning of each test.
Related
I'm trying to implement authentication & authorization of users between my microservices and API Gateway.What I have now:
API Gateway which can request to any microservice.
User microservice - where I'm storing all users. laravel/passport implemented to authenticate user in this microservice. Works as it should be, login route returns token which I'm using to authenticate user in this microservice.
Other 5 microservices without any authentication or authorization.
Question is: what is the right way to use authentication & authorization with microservices? I know that I should authenticate users in my API Gateway and authorization will happen inside microservices. But how authorization in other microservices happening if they don't know anything about users?
I'm planning to use somehow JWT token with information about user roles but haven't found yet how to put that information into token
I'll try to explain with a basic example for API.
Let's say you have currently 3 microservices :
Users
Posts
Core
I assume you're using httpOnly cookie to store user token.
In Core microservice I have this route structure:
Route::prefix('core')->group(function () {
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
Route::middleware('scope.trader')->group(function () {
Route::get('user', [AuthController::class, 'user']);
});
});
Now i want to login which i should send an API request, and I should think of a solution to send token anytime I need it.
login(this is where you get token) and register don't need token
user need token (this is where you asked for solution)
So in addition to get a result, I should create a service for user, and here how I've done it :
UserService :
class UserService extends ApiService
{
public function __construct()
{
// Get User Endpoint Microservice API URL
$this->endpoint = env('USERS_MS') . '/api';
}
}
ApiService :
abstract class ApiService
{
protected string $endpoint;
public function request($method, $path, $data = [])
{
$response = $this->getRequest($method, $path, $data);
if ($response->ok()) {return $response->json();};
throw new HttpException($response->status(), $response->body());
}
public function getRequest($method, $path, $data = [])
{
return \Http::acceptJson()->withHeaders([
'Authorization' => 'Bearer ' . request()->cookie('token')
])->$method("{$this->endpoint}/{$path}", $data);
}
public function post($path, $data)
{
return $this->request('post', $path, $data);
}
public function get($path)
{
return $this->request('get', $path);
}
public function put($path, $data)
{
return $this->request('put', $path, $data);
}
public function delete($path)
{
return $this->request('delete', $path);
}
}
If you're wondering where, this UserService come from, then I should say, I've created a package to use it in other microservices, so you can do the same or just create a service and use it in your microservices or etc.
Everything is obvious about ApiService, but I'll try to explain the base.
Anytime we want to do an API call, we can simply call Allowed methods in this class, then our methods, will call request, to pass common arguments, and eventually using those arguments to do the API call.
getRequest method, is doing the call and get the stored token from httpOnly cookie, and will send it as an Authorization header to the target endpoint, and eventually it'll return whatever it get from target.
So If we want to use this, we can simply do like this in our controller :
class AuthController extends Controller
{
// use Services\UserService;
public UserService $userService;
/**
* #param UserService $userService
*/
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
public function register(RegisterRequest $request)
{
$data = $request->only('name', 'email', 'password') + ['additional_fileds' => 0 ];
// additional fields can be used for something except from request and
// optional, like is it admin or user or etc.
// call the post method, pass the endpoint url(`register`), pass $data
$user = $this->userService->post('register', $data);
// get data from target endpoint
// and ...
return response($user, Response::HTTP_CREATED);
}
public function login(Request $request)
{
// same thing here again, but this time i passed scope to help me
// get the specific user scope
$data = $request->only('email', 'password') + ['scope' => 'writer'];
$response = $this->userService->post('login', $data);
// as you can see when user do success login, we will get token,
// which i got that token using Passport and set it to $cookie
$cookie = cookie('token', $response['token'], 60 * 24); // 1 day
// then will set a new httpOnly token on response.
return response([
'message' => 'success'
])->withCookie($cookie);
}
public function user(Request $request)
{
// Here, base on userService as you saw, we passed token in all requests
// which if token exist, we get the result, since we're expecting
// token to send back the user informations.
$user = $this->userService->get('user');
// get posts belong to authenticated user
$posts = Post::where('user_id', $user['id'])->get();
$user['posts'] = $posts;
return $user;
}
}
Now, how about user microservice? well Everything is clear here, and it should work like a basic app.
Here's the routes :
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
Route::middleware(['bunch','of', 'middlewares'])->group( function (){
Route::get('user', [AuthController::class, 'user']);
});
And in controller :
class AuthController extends Controller
{
public function register(Request $request)
{
$user = User::create(
$request->only('first_name', 'email', 'additional_field')
+ ['password' => \Hash::make($request->input('password'))]
);
return response($user, Response::HTTP_CREATED);
}
public function login(Request $request)
{
if (!\Auth::attempt($request->only('email', 'password'))) {
return response([
'error' => 'user or pass is wrong or whatever.'
], Response::HTTP_UNAUTHORIZED);
}
$user = \Auth::user();
$jwt = $user->createToken('token', [$request->input('here you can pass the required scope like trader as i expalined in top')])->plainTextToken;
return compact('token');
}
public function user(Request $request)
{
return $request->user();
}
}
So here's the complete example and you can use the Core microservice approach on other microservices to get your information related to authenticated user, and as you can see everything will be authenticated due to those requests from core to other microservices.
I have a login issue when performing tests on some controller for an API with Symfony 5.2.
All my endpoints are behind a firewall, and to test everything works fine, I need to login before I make a request.
I use a JWT authentication system with https://github.com/lexik/LexikJWTAuthenticationBundle
So I have this private method on my test class :
protected function logInAsUser(string $username): User
{
$tokenStorage = static::$client->getContainer()->get('security.token_storage');
$firewallName = 'api_area';
$userRepository = static::$container->get(UserRepository::class);
$user = $userRepository->findOneBy(['username' => $username]);
$token = new PostAuthenticationGuardToken($user, $firewallName, ['ROLE_USER']);
$tokenStorage->setToken($token);
return $user;
}
Then in my test methods, I call this method before making a request on a protected endpoint, like this :
public function testSomething() {
$this->logInAsUser('my-username');
static::$client->request('GET', '/api/protected/endpoint/');
$this->assertEquals(200, static::$client->getResponse()->getStatusCode());
}
And it works fine
(without calling $this->logInAsUser('my-username); I got a 401 response as intended)
But when I try to make two requests in the same test method, the second one fails with a 401, and the error message is : JWT Token not found
Example:
public function testSomething() {
$this->logInAsUser('my-username');
static::$client->request('GET', '/api/protected/endpoint/');
$this->assertEquals(200, static::$client->getResponse()->getStatusCode()); // OK
static::$client->request('GET', '/api/another/protected/endpoint/');
$this->assertEquals(200, static::$client->getResponse()->getStatusCode()); // FAILURE
}
I tried to re login before the second call, but it doesn't change anything.
Since Symfony 5.1 the Client used in WebTestcases provides a built-in method to mock a logged in user:
public function loginUser($user, string $firewallContext = 'main'): self
You should try using that one instead of doing it yourself.
public function testSomething()
{
$client = static::createClient();
$testUser = $userRepository->findOneByUsername('my-username');
$client->loginUser($testUser);
$client->request('GET', '/api/protected/endpoint/');
$this->assertEquals(200, $client->getResponse()->getStatusCode()); // OK
$client->request('GET', '/api/another/protected/endpoint/');
$this->assertEquals(200, $client->getResponse()->getStatusCode()); // FAILURE
}
I have one problem with Laravel Socialite login, in my Chrome works normally but in rest of the people browser doesn't work (works in other browsers). Before php update in server to 7.3.18 from 7.1 and update to Laravel 6 from 5.8, all works normally. I try to clear all caches, change session mode to cookie(file before), clear session and cookies in browser but nothing solved the problem.
When try to login, give me this
And this is my code:
public function loginSocial(Request $request){
$this->validate($request, [
'social_type' => 'required|in:google,facebook'
]);
$socialType = $request->get('social_type');
return Socialite::driver($socialType)->stateless()->redirect();
}
public function loginCallback(Request $request){
$socialType = $request->session()->get('social_type');
//Aparently, this get give to $socialType null in ppl browser. I dont understand why this get doesn't works.
$userSocial = Socialite::driver($socialType)->stateless()->user();
//If use 'google' instead $socialType, works fine.
$user = User::where('email',$userSocial->email)->first();
\Auth::login($user);
return redirect()->intended($this->redirectPath());
}
I understand what you are trying to do but sometimes less is more and more is less..... the call back is being made by the provider and not the user. anyway have different methods for each social login
// Google login
public function googleSocialLogin(Request $request){
Socialite::driver('google')->stateless()->redirect();
}
// Google callback
public function googleSocialLoginCallback(){
$userSocial = Socialite::driver('google')->stateless()->user();
$user = User::where('email',$userSocial->email)->first();
\Auth::login($user);
return redirect()->intended($this->redirectPath());
}
// Facebook login
public function facebookSocialLogin(Request $request){
Socialite::driver('facebook')->stateless()->redirect();
}
// Facebook callback
public function facebookSocialLoginCallback(){
$userSocial = Socialite::driver('facebook')->stateless()->user();
$user = User::where('email',$userSocial->email)->first();
\Auth::login($user);
return redirect()->intended($this->redirectPath());
}
With your methods separated you will have different routes for different social login which IMO is far better as they are have slightly different return params and you may want to perform additional function for a particular social login in future.
I'm strugling with authorization middleware in Slim4. Here's my code:
$app = AppFactory::create();
$app->add(new Authentication());
$app->group('/providers', function(RouteCollectorProxy $group){
$group->get('/', 'Project\Controller\ProviderController:get');
})->add(new SuperuserAuthorization());
Authentication middleware checks the user and works fine.
The method get in ProviderController is
public function get(Request $request, Response $response): Response{
$payload = [];
foreach(Provider::all() as $provider){
$payload[] = [
'id' => $provider->id,
'name' => $provider->name,
];
}
$response->getBody()->write(json_encode($payload));
return $response;
}
The SuperuserAuthorization looks like this
class SuperuserAuthorization{
public function __invoke(Request $request, RequestHandler $handler): Response{
$response = $handler->handle($request);
$authorization = explode(" ", $request->getHeader('Authorization')[0]);
$user = User::getUserByApiKey($authorization[1]);
if(! Role::isSuperuser($user)){
return $response->withStatus(403);//Forbidden
}
return $response;
}
}
The thing is that even though the user is not a superuser, the application continues executing. As a result I get json with all the providers and http code 403 :/
Shouldn't route middleware stop the request from getting into the app and just return 403 right away?
I know that I can create new empty response with status 403, so the data won't come out, but the point is that the request should never get beyond this middleware, am I right or did I just misunderstand something hereā¦
Any help will be appreciated :)
------------- SOLUTION ----------------
Thanks to #Nima I solved it. The updated version of middleware is:
class SuperuserAuthorization{
public function __invoke(Request $request, RequestHandler $handler): Response{
$authorization = explode(" ", $request->getHeader('Authorization')[0]);
$user = User::getUserByApiKey($authorization[1]);
if(! Role::isSuperuser($user)){
$response = new Response();
return $response->withStatus(403);//Forbidden
}
return $handler->handle($request);
}
}
Shouldn't route middleware stop the request from getting into the app and just return 403 right away?
Slim 4 uses PSR-15 compatible middlewares. There is good example of how to implement an authorization middleware in PSR-15 meta document. You need to avoid calling $handler->handle($request) if you don't want the request to be processed any further.
As you can see in the example, if the request is not authorized, a response different from the return value of $handler->handle($request) is returned. This means your point saying:
I know that I can create new empty response with status 403, so the data won't come out, but the point is that the request should never get beyond this middleware
is somehow correct, but you should prevent the request from going further by returning appropriate response before invoking the handler, or throwing an exception and let the error handler handle it.
Here is a simple middleware that randomly authorizes some of requests and throws an exception for others:
$app->group('/protected', function($group){
$group->get('/', function($request, $response){
$response->getBody()->write('Some protected response...');
return $response;
});
})->add(function($request, $handler){
// randomly authorize/reject requests
if(rand() % 2) {
// Instead of throwing an exception, you can return an appropriate response
throw new \Slim\Exception\HttpForbiddenException($request);
}
$response = $handler->handle($request);
$response->getBody()->write('(this request was authorized by the middleware)');
return $response;
});
To see different responses, please visit /protected/ path a few times (remember the middleware acts randomly)
Laravel Version 5.2
In my project, users with role_id = 4 has the admin role and can manage users.
I have defined the following ability in AuthServiceProvider:
public function boot(GateContract $gate)
{
$this->registerPolicies($gate);
$gate->define('can-manage-users', function ($user)
{
return $user->role_id == 4;
});
}
I have used this ability in the UserController __construct method as follows:
public function __construct()
{
$this->authorize('can-manage-users');
}
In ExampleTest, I have created two tests to check if the defined authorization works.
The first test for admin user who has role_id = 4. This test passes.
public function testAdminCanManageUsers()
{
$user = Auth::loginUsingId(1);
$this->actingAs($user)
->visit('users')
->assertResponseOk();
}
The second test is for another user who does not have role_id = 4. I have tried with response status 401 and 403. But the test is failing:
public function testNonAdminCannotManageUsers()
{
$user = Auth::loginUsingId(4);
$this->actingAs($user)
->visit('users')
->assertResponseStatus(403);
}
First few lines of the failure message is given below:
A request to [http://localhost/users] failed. Received status code [403].
C:\wamp\www\laravel\blog\vendor\laravel\framework\src\Illuminate\Foundation\Testing\Concerns\InteractsWithPages.php:196
C:\wamp\www\laravel\blog\vendor\laravel\framework\src\Illuminate\Foundation\Testing\Concerns\InteractsWithPages.php:80
C:\wamp\www\laravel\blog\vendor\laravel\framework\src\Illuminate\Foundation\Testing\Concerns\InteractsWithPages.php:61
C:\wamp\www\laravel\blog\tests\ExampleTest.php:33
Caused by exception 'Illuminate\Auth\Access\AuthorizationException'
with message 'This action is unauthorized.' in
C:\wamp\www\laravel\blog\vendor\laravel\framework\src\Illuminate\Auth\Access\HandlesAuthorization.php:28
I have also tried to use 'see' method as follows:
public function testNonAdminCannotManageUsers()
{
$user = Auth::loginUsingId(4);
$this->actingAs($user)
->visit('users')
->see('This action is unauthorized.');
}
But it's failing too. What am I doing wrong? How can I make the test pass?
The mistake is calling the visit method. The visit method is in the InteractsWithPages trait. This method calls the makeRequest method which in turn calls assertPageLoaded method. This method gets the status code returned and if it gets code other than 200, it catches a PHPUnitException and throws an HttpException with the message
"A request to [{$uri}] failed. Received status code [{$status}]."
This is why the test was failing with the above message.
The test can be successfully passed by using get method instead of visit method. For example:
public function testNonAdminCannotManageUsers()
{
$user = App\User::where('role_id', '<>', 4)->first();
$this->actingAs($user)
->get('users')
->assertResponseStatus(403);
}
This test will pass and confirm that a non admin user cannot access the url.
Since the Auth middleware redirects to a login route when unauthenticated by default you could also perform the following test:
public function testNonAdminCannotManageUsers()
{
$user = Auth::loginUsingId(4);
$this->actingAs($user)
->visit('users')
->assertRedirect('login');
}
Since at least Laravel 5.4, you'll want to use the assertStatus(403) method.
public function testNonAdminCannotManageUsers()
{
$user = Auth::loginUsingId(4);
$this->actingAs($user)
->visit('users')
->assertStatus(403);
}