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 Laravel API that goes thru middleware and works just fine when called externally.
public function apiMethod(Request $request)
{
$requestData = $request->all();
dd($requestData);
}
In addition, I have a form that is processed by a method in a separate controller. In this method, I do a request to the API:
public function formHandler(Request $request)
{
$request = \Request::create('/path/to/the/api/', 'POST', ['A' => 1]);
$response = \Route::dispatch($request)->getContent();
}
In this case, $requestData contains the form parameters instead of the array that I pass with my API request (['A' => 1]).
So I wonder what's the right approach to solve that issue in Laravel?
In my Laravel 5.8 app I have many API routes which return paginated results. If I make a request to my API appending the following query string I can disable pagination.
http://api.test/users/?no_paginate=1
My question is... how can I disable no_paginate from being used on certain routes? I'd preferbly want some validation to go in the request class but I can't find anything in the docs for that.
You can do this using a Global Middleware.
Create a DisableNoPaginate Middleware:
php artisan make:middleware DisableNoPaginate
Then define what the middleware should do (DisableNoPaginate.php):
<?php
namespace App\Http\Middleware;
use Closure;
class DisableNoPaginate
{
public function handle($request, Closure $next)
{
//remove no_paginate param from request object
unset($request['no_paginate']);
return $next($request);
}
}
Arrange for the middleware to run on all routes (routes.php):
$app->middleware([
App\Http\Middleware\DisableNoPaginate::class
]);
Now the no_paginate query param should be stripped from all your incoming requests.
For the best approach to get users either paginate or get all listing by below code in UsersController
public function index($type = null, Request $request)
{
$builder = User::where(/*query*/);
if($type == "paginate") {
$items = $builder->paginate(10);
} else {
$items = $builder->get();
}
return view("users.index", ['users' => $items]);
}
Here is the route in web.php/api.php file
Route::get('/{type?}', ['as' => 'users.index', 'uses' => 'UsersController#index']);
Here url will be
http://api.test/users/paginate // get pagination response.
http://api.test/users // get response without pagination
I think this will help you.
I want to remove an existing item from a request parameters that passes in a controller.
Here's my controller:
public function getIndex(Request $request)
{
// I need to remove a parameter from the $request here.
}
Actually, I want to dispatch a request in a controller but when I make a new instance of Request like this,
$new_request = new Request();
and add some fields to the $new_request like this:
$request->request->add([
'id' => '2',
'name' => 'test'
]);
Nothing is added! and the dispatch method can not yield a correct response with empty request!
But when I use an existing route request, every thing is ok, except extra items and I should get rid of them!
Do you need this?
public function getIndex(Request $request)
{
$request->request->remove('yourParamName');
}
I'm trying to implement json-schema validator from justinrainbow as middleware in Slim 3.
I can't figure out how to get the clients input from GET/POST requests in middleware.
tried like this:
$mw = function ($request, $response, $next) {
$data = $request->getParsedBody();
print_r($data); // prints nothing
$id = $request->getAttribute('loan_id');
print_r($id); // prints nothing
// here I need to validate the user input from GET/POST requests with json-schema library and send the result to controller
$response = $next($request, $response);
return $response;
};
$app->get('/loan/{loan_id}', function (Request $request, Response $response) use ($app, $model) {
$loanId = $request->getAttribute('loan_id'); // here it works
$data = $model->getLoan($loanId);
$newResponse = $response->withJson($data, 201);
return $newResponse;
})->add($mw);
There are 2 possible ways of how I need it. what i'm doing wrong ?
validate it in middleware and send some array/json response to the controller, which i will then get as I understood with $data = $request->getParsedBody();
validate it in middleware but final check will be in controller like this:
$app->get('/loan/{loan_id}', function (Request $request, Response $response) use ($app, $model) {
if($validator->isValid()){
//
}
$loanId = $request->getAttribute('loan_id'); // here it works
$data = $model->getLoan($loanId);
$newResponse = $response->withJson($data, 201);
return $newResponse;
})->add($mw);
Best option for me it do something like here
but I don't understand what should i return in container, and how to pass get/post input to container
Your code in the first point seems alright, you only try to access route parameter from within middleware. At that point the route is not yet resolved and therefore parameters are not parsed from the URL.
This is a known use case and is described in Slim's documentation. Add the following setting to your app configuration to get your code working:
$app = new App([
'settings' => [
// Only set this if you need access to route within middleware
'determineRouteBeforeAppMiddleware' => true
]
]);
In order to understand how middleware works and how to manipulate response object, I suggest you read the User Guide - it's not that long and explains it really well.