Laravel Passport Unit test for logout / revoking token - php

I am using Laravel Passport and have a logout function in the controller. See below:
public function logout(Request $request){
auth()->user()->token()->revoke();
$response = ['message' => 'You have been successfully logged out!'];
return response($response, 200);
}
I am now trying to write a unit test for this but the user stays logged in even after the logout and the token being revoked. I have found this Method Illuminate\Auth\RequestGuard::logout does not exist Laravel Passport but even this solution does not work for me. I am guessing it might be because I am using Laravel 8. My Unit test looks like this:
public function testLogout()
{
//Random email and password
$email = $this->faker->email;
$password = $this->faker->password(8);
//Create a user
$this->createUser($this->faker->name, $email, $password);
//Data for the post request
$data = [
'email' => $email,
'password' => $password
];
//Try login
$response = $this->json('POST','api/login', $data);
//Assert it was successful
$response->assertStatus(200);
//Assert we received a token
$this->assertArrayHasKey('token', $response->json());
//Get the token
$token = $response->json()['token'];
//Setup authenticated header
$header = [
'Authorization' => 'Bearer '.$token
];
//try to access authenticated route
$response = $this->json('get', 'api/ads', [], $header);
//Assert it was successful
$response->assertStatus(200);
$this->resetAuth();
//Logout the user
$response = $this->post('api/logout', [], $header);
//Assert it was successful
$response->assertStatus(200);
//try to access authenticated route
$response = $this->json('get', 'api/ads', [], $header);
//Assert it returns unathorized error
$response->assertStatus(401);
//Delete the user
User::where('email', $email)->delete();
}
And the result is the following:
Expected status code 401 but received 200.
Failed asserting that 401 is identical to 200.

You are doing too much in a single test. But other than that, I couldn't really spot any issues in your code. But this worked for me:
class LogoutController extends Controller
{
public function __construct()
{
$this->middleware('auth:api');
}
public function logout()
{
Auth::user()->token()->revoke();
$tokenId = Auth::user()->token()->id;
$refreshTokenRepository = app('Laravel\Passport\RefreshTokenRepository');
$refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId);
return response(null, Response::HTTP_NO_CONTENT);
}
}
And the test can simply do something like:
public function testAnAuthenticatedUserCanLogout()
{
$user = User::factory()->create();
Passport::actingAs($user);
$this->postJson('/api/logout')
->assertNoContent();
}

Your logout method looks strange. I would do this
use Illuminate\Support\Facades\Auth;
// ... other controller methods
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
// ...do a redirect or other stuff
}
to logout out. This should properly logout the user. This is also the proposed way in the Laravel docs.
For logging out from Laravel Passport you can run
if (Auth::check()) {
Auth::user()->token()->revoke();
}
to revoke the current token used. This will definitely log the user out from the current device where he requested to log out.
Make sure in your routes/web.php you have your logout route inside the group(['middleware' => 'auth:api'] group.
If you want to log out from all the devices where he's logged in, you can also remove the token from database.

Related

How can i fix "not logged in" error in laravel even after logging in?

Im making a gym app on laravel 9 as an api. The app will have a normal user and also an admin which has access to certain files that a normal user doesnt. For that i created a middleware but im facing a few problems there.
See even after i log in, when i try using a route protected by my isAdmin middleware, i get the error: Log in to gain access. This is the Login function:
public function login(Request $request)
{
$creds = $request->validate([
'email' => 'required|email|string|exists:users,email',
'password' => ['required']
]);
if (!Auth::attempt($creds)) {
return response([ 'message' => 'Provided email or password is incorrect' ], 422);
}
/** #var \App\Models\User $user */
$user = Auth::user();
Auth::login($user);
$token = $user->createToken('main')->plainTextToken;
return response(compact('user', 'token'))->header('Authorization', 'Bearer '.$token);
}
and then this is the middleware
public function handle(Request $request, Closure $next)
{
if (Auth::check()) {
$user = Auth::guard('auth')->user();
dd($user);
if ($user->role == 1) {
return $next($request);
} else{
return response(['msg'=>'You are not authorized for this route!']);
}
} else{
return response(['msg'=>'Log In to gain access!']);
}
return $next($request);
}
Im pretty sure the problem has to do with the token either not being sent or being stored properly, but i cant seem to find where the problem is. Also this is what im doing in Postman if that would help:
Login
Add Recipe (this is a route guarded by the middleware)
And also here are their headers respectively
Also i dont know if this is info that will help but i have to manually write the that bearer token for it to show up. (im a beginner so idk how it should be done)

How to implement authentication & authorization between microservices & API Gateway using Laravel

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.

session for authenticated user is not being set

(I'm aware that the following example isn't safe)
I have a basic authentication system where a user has to enter his credentials. When the entered credentials are correct I set in my laravel application a session, which indicates that he can perform authenticated requests to the api. I do that the following way:
public function authenticate(Request $request){
$data = array(
'name' => $request->input('UserName'),
'password' =>$request->input('Password'),
'email' => 'test#test.ch'
);
If(Auth::attempt($data)){
$request->session()->put('isAuthenticated',true);
$request->session()->save();
return "success";
}
return "wrong";
}
My middleware where I check if the user is allowed to make requests:
public function handle($request, Closure $next){
if(!empty($request->session()->get('isAuthenticated')) && $request->session()->get('isAuthenticated') === true){
return $next($request);
}
return redirect('/');
}
My routes:
Route::post('/login', 'UserController#authenticate');
Route::group(['middleware' =>['web','check_auth']], function (){
Route::get('/logs','LogController#getAllLogEntries');
Route::get('/logs/{id}','LogController#getLogEntryById');
});
My problem:
Whenever a user logs in with the right credentials the server returns "success" as response, but always directs me back to the base root ('/'). That makes me assume that the session doesn't get set. How can I fix this error?

Laravel Can't retrieve session data saved in POST request

I'm having an issue with Laravel session create and read.
I can create session values , but problem is access the data.
If I create an session value in POST request, then I can't access it on GET request.
Here is my route
Route::get('/', 'HomeController#index');
Route::get('/admin/dashboard', 'AdminController#dashBoard');
Route::post('/admin/login_admin', 'AdminController#doLogin');
Route::post('/admin/login_test', 'AdminController#test');
Route::get('/admin/login', 'AdminController#seeLogin');
Here the doLogin using post method:
public function doLogin(Request $request){
$email = $request->input("email");
$password = $request->input("password");
$checkerInfo = AdminGetLoginChecker($email, $password);
//if logged in
if($checkerInfo){
$request->session()->put('loggedIn', '1');
$request->session()->put('userId', $checkerInfo);
$request->session()->save();
return response()->json([
'success' => true,
'message' => 'logged In'
]);
}
return response()->json([
'success' => false,
'message' => 'Incorrect User Email, Password'
]);
}
Now problem is if request to access the session value in other get request section, then it doesn't allow to access.
public function dashboard(Request $request)
{
$data = $request->session()->all();
print_r($data);
return view('welcome', ['name' => 'James']);
}
Here it doesn't show the saved session data, because it's a GET request.
Now if try to get request in any another method using POST request it'll show the saved data earlier in doLogin Method
//it's a POST requset
public function test(Request $request)
{
$data = $request->session()->all();
print_r($data);
}
How can I access the session data in GET requested methods?
Thanks in Advance,
Update:
Please note that add the url "'/admin/login_admin'" in excepts area of verifyCsrfToken middelwere to avoid csrf token issue while posting data
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
class VerifyCsrfToken extends BaseVerifier
{
/**
* The URIs that should be excluded from CSRF verification.
*
* #var array
*/
protected $except = [
//
'url' => '/admin/login_admin'
];
}
Why are you trying to manually authenticate user?
For session operation use "Session" facade.
eg:
Session::set('your_session_key', 'Session Value');
Session::save();
$yourSessionKey = Session::get('your_session_key');
let's try it like this
$input = $request->all();
// imagine you have Admin.php model
$query = Admin::login($input['email'], $input['password'])->first();
if ($query)
{
Session::put('userId', $query->id);
Session::save();
return "u're in";
}
else{
return "User name or password is wrong";
}
and in your test function
return $request->session()->get('userId');

Laravel running tests as different users

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.

Categories