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

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.

Related

My login route returns "unauthorised access" Laravel

So I'm trying to make a laravel API for a escorts-like site, anyway, i use Passport for authentification and the register part works but the login one doesnt, and i dont know why, i'll let the passportAuthController down as code and a ss of the database
class passportAuthController extends Controller
{
/**
* handle user registration request
*/
public function registerUserExample(RegisterUserRequest $request){
///TODO: TEST THE CRUD FEATURES IMPLEMENTED IN THE USER CONTROLLER AFTER U CHECK LOGIN FEATURE
$attributes = $request -> validated();
$user = User::create($attributes);
$access_token_example = $user->createToken('RegisterToken')->accessToken;
//return the access token we generated in the above step
return response()->json(['token'=>$access_token_example],200);
}
/**
* login user to our application
*/
public function loginUserExample(Request $request){
$login_credentials=[
'email'=>$request->email,
'password'=>$request->password,
];
if(auth()->attempt($login_credentials)){
//generate the token for the user
$user_login_token= auth()->user()->createToken('LoginToken')->accessToken;
//now return this token on success login attempt
return response()->json(['token' => $user_login_token], 200);
}
else{
//wrong login credentials, return, user not authorised to our system, return error code 401
return response()->json(['error' => 'UnAuthorised Access'], 401);
}
}
/**
* This method returns authenticated user details
*/
// index function
public function authenticatedUserDetails(){
//returns details
return response()->json(['authenticated-user' => auth()->user()], 200);
}
}
The request as well:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class RegisterUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'name'=>'required|max:255|min:3',
'email'=>'required|email',
'password'=>'required|min:7|max:255',
'gender'=>'required|min:4|max:6',
'interest'=>'required|min:4|max:6',
'Country'=>'required|max:255',
'County'=>'required|max:255',
'City'=>'required|max:255',
'birthday'=>'required|date'
];
}
}
and the ss of the database:
and the routes (api.php):
<?php
use App\Http\Controllers\passportAuthController;
use App\Http\Controllers\UserController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
//routes/api.php
//login & register routes
Route::post('register',[passportAuthController::class,'registerUserExample']);
Route::post('login',[passportAuthController::class,'loginUserExample']);
//CRUD and search routes
Route::post('storeUser',[UserController::class,'store']);
Route::get('showAll',[UserController::class, 'index']);
Route::put('updateUser/{id}',[UserController::class,'update']);
Route::delete('delete/{id}', [UserController::class,'deleteUser']);
Route::get('search/{name}',[UserController::class,'search']);
//add this middleware to ensure that every request is authenticated
Route::middleware('auth:api')->group(function(){
Route::get('user', [passportAuthController::class,'authenticatedUserDetails']);
});
Your password in users table is not encrypted.
The reason is this line
$attributes = $request->validated();
$user = User::create($attributes);
You have not encrypted your password and the method auth()->attempt($login_credentials) uses compares the encrypted password request with stored encrypted password in your db.
You can use bcrpyt() to encrypt your password, laravel comes with bcrypt() as a helper function.
Change to this in your registerUserExample(RegisterUserRequest $request)
$attributes = $request->validated();
foreach($attributes as $key => $attribute){
if($key == 'password') {
$attributes[$key] = bcrypt($attribute);
}
}
$user = User::create($attributes);
so if you see the response is mean that wrong login credentials, return, user not authorised to our system, return error code 401 ,
So with a little observation you will know that your code work fine but your logic is not good ,
So the answer simply is because the password insert in your database is note crypted and laravel passport when they are trying to make login they use a function of check ,
so if you want your code work your password must be crypted in the register exemple
$user->password = hash::make($request->password);
Or
$user->password = Crypt::encrypt($request->password);
Conclusion you can't make authentification with laravel passport if your password not crypted
The attempt method accepts an array of key / value pairs as its first argument. The password value will be hashed. The other values in the array will be used to find the user in your database table. So,
You try this
public function loginUserExample(Request $request){
$user = User::where('account', $request->account)
->where('password', $request->password)
->first();
if($user) {
Auth::loginUsingId($user->id);
// -- OR -- //
Auth::login($user);
return redirect()->route('home');
} else {
return redirect()->back()->withInput();
}
}

Laravel Passport Unit test for logout / revoking token

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.

Profile Settings middleware

I have a user profile page and user profile/settings page
the problem is I made a middleware for settings page to prevent any auth user from entering other users settings page or update them Unless the ID OR SLUG IS MATCHED to the auth user but I'm using Vue whenever I use the API routes to fetch or update the data it says unauthorized 401 or 500.
middleware :
public function handle($request, Closure $next)
{
if ($request->slug != auth()->user()->slug) {
return redirect()->to('/');
}
return $next($request);
}
API route :
Route::get('/profile/{slug}','ProfilePrivateController#show')->middleware('editProfile');;
VueJs :
update(){
axios.put(`/api/profile/${this.id}`,{
email : this.email,
username : this.name,
password : this.password,
education_level : this.education_level,
fb_url : this.fb_url,
twitter_url : this.twitter_url,
field : this.field
})
.then(res=>console.log(res))
}
Controller :
public function show($slug)
{
$user = User::findBySlugOrFail($slug);
return response()->json($user);
}
public function update(Request $request, $slug)
{
$user = User::findBySlug($slug);
$user->update([
'email'=>$request->email,
'education_level'=>$request->education_level,
'field'=>$request->field,
'school'=>$request->school,
'fb_url'=>$request->fb_url,
'twitter_url'=>$request->twitter_url,
]);
if($request->has('password')){
$user->save([
'password'=>$request->password
]);
}
return response()->json('user updated',200);
}
I Want to let the user update his settings and secure the API at the same time.
I'm really lost at this point Any help is appreciated!
You have a GET request for the API route, but using a PUT request in Vue.
Updating Route::get to Route::put should solve the problem.
Also, since its an AJAX request, you should be returning a JSON response so it can easily be consumed. You can return something similar to:
return response()->json(['error' => 'unauthorized'], 401);

Refreshing authentication tokens for a Vue.js SPA using Laravel for the backend

I am building a single-page-app with Vue (2.5) using Laravel (5.5) as the backend. Everything works well, except for directly logging in again after having logged out. In this case, the call to /api/user (to retrieve the user's account information and to verify the user's identity once more) fails with a 401 unauthorized (even though the log-in succeeded). As a response, the user is bounced back directly to the login screen (I wrote this measure myself as a reaction to 401 responses).
What does work is to log out, refresh the page with ctrl/cmd+R, and then log in again. The fact that a page refresh fixes my problem, gives me reason to believe that I am not handling refresh of the X-CSRF-TOKEN correctly, or may be forgetting about certain cookies that Laravel uses (as described here ).
This is a snippet of the code of the login form that is executed after a user clicks the login button.
login(){
// Copy the form data
const data = {...this.user};
// If remember is false, don't send the parameter to the server
if(data.remember === false){
delete data.remember;
}
this.authenticating = true;
this.authenticate(data)
.then( this.refreshTokens )
.catch( error => {
this.authenticating = false;
if(error.response && [422, 423].includes(error.response.status) ){
this.validationErrors = error.response.data.errors;
this.showErrorMessage(error.response.data.message);
}else{
this.showErrorMessage(error.message);
}
});
},
refreshTokens(){
return new Promise((resolve, reject) => {
axios.get('/refreshtokens')
.then( response => {
window.Laravel.csrfToken = response.data.csrfToken;
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = response.data.csrfToken;
this.authenticating = false;
this.$router.replace(this.$route.query.redirect || '/');
return resolve(response);
})
.catch( error => {
this.showErrorMessage(error.message);
reject(error);
});
});
},
the authenticate() method is a vuex action, which calls the login endpoint at the laravel side.
The /refreshTokens endpoint simply calls this Laravel controller function that returns the CSRF token of the currently logged-in user:
public function getCsrfToken(){
return ['csrfToken' => csrf_token()];
}
After the tokens have been refetched, the user is redirected to the main page (or another page if supplied)
with this.$router.replace(this.$route.query.redirect || '/'); and there the api/user function is called to check the data of the currently logged in user.
Are there any other measures I should take to make this work, that I am overlooking?
Thanks for any help!
EDIT: 07 Nov 2017
After all the helpful suggestions, I would like to add some information. I am using Passport to authenticate on the Laravel side, and the CreateFreshApiToken middleware is in place.
I have been looking at the cookies set by my app, and in particular the laravel_token which is said to hold the encrypted JWT that Passport will use to authenticate API requests from your JavaScript application. When logging out, the laravel_token cookie is deleted. When logging in again directly afterwards (using axios to send an AJAX post request) no new laravel_token is being set, so that's why it doesn't authenticate the user. I am aware that Laravel doesn't set the cookie on the login POST request, but the GET request to /refreshTokens (which is not guarded) directly afterwards should set the cookie. However, this doesn't appear to be happening.
I have tried increasing the delay between the request to /refreshTokens and the request to /api/user, to maybe give the server some time to get things in order, but to no avail.
For completeness sake, here is my Auth\LoginController that is handling the login request server-side:
class LoginController extends Controller
{
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* #var string
*/
protected $redirectTo = '/';
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
// $this->middleware('guest')->except('logout');
}
/**
* Get the needed authorization credentials from the request.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
protected function credentials(\Illuminate\Http\Request $request)
{
//return $request->only($this->username(), 'password');
return ['email' => $request->{$this->username()}, 'password' => $request->password, 'active' => 1];
}
/**
* The user has been authenticated.
*
* #param \Illuminate\Http\Request $request
* #param mixed $user
* #return mixed
*/
protected function authenticated(\Illuminate\Http\Request $request, $user)
{
$user->last_login = \Carbon\Carbon::now();
$user->timestamps = false;
$user->save();
$user->timestamps = true;
return (new UserResource($user))->additional(
['permissions' => $user->getUIPermissions()]
);
}
/**
* Log the user out of the application.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function logout(\Illuminate\Http\Request $request)
{
$this->guard()->logout();
$request->session()->invalidate();
}
}
Considering that you are using an api for authentication, I would suggest using Passport or JWT Authentication to handle authentication tokens.
Finally fixed it!
By returning the UserResource directly in the LoginControllers authenticated method, it is not a valid Laravel Response (but I guess raw JSON data?) so probably things like cookies are not attached. I had to attach a call to response() on the resource and now everything seems to work fine (though I need to do more extensive testing).
So:
protected function authenticated(\Illuminate\Http\Request $request, $user)
{
...
return (new UserResource($user))->additional(
['permissions' => $user->getUIPermissions()]
);
}
becomes
protected function authenticated(\Illuminate\Http\Request $request, $user)
{
...
return (new UserResource($user))->additional(
['permissions' => $user->getUIPermissions()]
)->response(); // Add response to Resource
}
Hurray for the Laravel docs on attributing a section to this:
https://laravel.com/docs/5.5/eloquent-resources#resource-responses
Additionally, the laravel_token is not set by the POST request to login, and the call to refreshCsrfToken() also didn't do the trick, probably because it was protected by the guest middleware.
What worked for me in the end is to perform a dummy call to '/' right after the login function returned (or the promise was fulfilled).
In the end, my login function in the component was as follows:
login(){
// Copy the user object
const data = {...this.user};
// If remember is false, don't send the parameter to the server
if(data.remember === false){
delete data.remember;
}
this.authenticating = true;
this.authenticate(data)
.then( csrf_token => {
window.Laravel.csrfToken = csrf_token;
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = csrf_token;
// Perform a dummy GET request to the site root to obtain the larevel_token cookie
// which is used for authentication. Strangely enough this cookie is not set with the
// POST request to the login function.
axios.get('/')
.then( () => {
this.authenticating = false;
this.$router.replace(this.$route.query.redirect || '/');
})
.catch(e => this.showErrorMessage(e.message));
})
.catch( error => {
this.authenticating = false;
if(error.response && [422, 423].includes(error.response.status) ){
this.validationErrors = error.response.data.errors;
this.showErrorMessage(error.response.data.message);
}else{
this.showErrorMessage(error.message);
}
});
and the authenticate() action in my vuex store is as follows:
authenticate({ dispatch }, data){
return new Promise( (resolve, reject) => {
axios.post(LOGIN, data)
.then( response => {
const {csrf_token, ...user} = response.data;
// Set Vuex state
dispatch('setUser', user );
// Store the user data in local storage
Vue.ls.set('user', user );
return resolve(csrf_token);
})
.catch( error => reject(error) );
});
},
Because I didn't want to make an extra call to refreshTokens in addition to the dummy call to /, I attached the csrf_token to the response of the /login route of the backend:
protected function authenticated(\Illuminate\Http\Request $request, $user)
{
$user->last_login = \Carbon\Carbon::now();
$user->timestamps = false;
$user->save();
$user->timestamps = true;
return (new UserResource($user))->additional([
'permissions' => $user->getUIPermissions(),
'csrf_token' => csrf_token()
])->response();
}
You should use Passports CreateFreshApiToken middleware in your web middleware passport consuming-your-api
web => [...,
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],
this attaches attach the right csrftoken() to all your Request headers as request_cookies

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