Laravel Middleware - Skip Passport Auth - php

We have 2 auth middlewares applied to specific routes, 'external_token' and 'auth:api'. When an external bearer token is presented we inspect it, and if all the values are good we consider the user authorized to access the requested url.
How do we process all other middlewares except passport auth?
public function handle(Request $request, Closure $next)
{
$token = $request->header('Bearer');
try {
list($JWTHeader, $JWTPayload) = JWT::verify($token, JWT::TYPE_ID_EXTERNAL);
$this->user = User::where('external_id', $JWTPayload['external_id'])->first();
// Can we just set $this->user and process all other middlewares except auth?
} catch (Exception $e) {
Log::debug($e);
}
$response = $next($request);
return $response;
}

Well, one thing you could do would be to set the user on the api guard, so when the auth middleware runs, it'll find the user you provided. You would have to ensure that your external_token middleware runs first.
auth()->guard('api')->setUser($this->user);
Another option would be to convert your external_token middleware into a Laravel auth guard so that you can use the built-in auth functionality. Then, you can protect your route with auth:api,external_token, and the auth will pass if any one of the specified guards is successful.
The simplest example would be a closure request guard.
In your AuthServiceProvider::boot() method:
// don't forget your "use" statements for all these classes
public function boot()
{
// ...
Auth::viaRequest('external_token_driver', function ($request) {
$token = $request->header('Bearer');
try {
list($JWTHeader, $JWTPayload) = JWT::verify($token, JWT::TYPE_ID_EXTERNAL);
return User::where('external_id', $JWTPayload['external_id'])->first();
} catch (Exception $e) {
Log::debug($e);
}
return null;
});
}
In your auth.php config:
'guards' => [
// web, api, etc...
'external_token' => [
'driver' => 'external_token_driver',
],
],
NB: all untested.

Related

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.

Laravel 5.8 - How to handle API route with optional auth?

Can anyone help me to handle API route with optional Auth check? I have an API ROUTE 'apply-coupon' which can be used by both 'Guest' and 'Logged In' user. I need to get User ID in controller if the user is logged in.
Front end - Is developed with React JS, on 'Apply Coupon' button click, an API is calling as -
API end point - https://example.com/api/apply-coupon
Request payload - {coupon : 'my-coupon-code', shop_id : '1', subtotal : '150'}
Note : No token is passed in the header, since 'apply coupon' feature is available for guest user as well.
Expected output :- When user clicks on 'Apply Coupon' it should return User ID, if user id logged in.
Following is my route in api.php file -
Route::post('/apply-coupon', [
'uses' => 'CouponController#applyCoupon',
]);
Update :- And the default guard is -
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
I have tried
if(Auth::user()) {
dd(Auth::user());
}
and
auth()->guard('api')->user()
But nothing worked. Thanks!!
I don't think you can "optionaly" protect routes. What I think you could do is check in your controller if the user is authenticated and if it is, then get its ID. Of course the route should be unprotected
use Illuminate\Support\Facades\Auth;
...
function applyCoupon() {
if (Auth::check()) {
// The user is logged in...
// return Cupon + Auth::id()
} else {
// Not logged in code
// return Cupon
}
}
docs
Add one middleware ValidateUser.php
namespace App\Http\Middleware;
use Closure;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Tymon\JWTAuth\Exceptions\TokenInvalidException;
use Tymon\JWTAuth\Facades\JWTAuth;
class ValidateUser
{
public function handle($request, Closure $next)
{
if (! $request->bearerToken()) {
return $next($request);
}
return $this->validateToken($request, $next);
}
private function validateToken($request, Closure $next)
{
try {
if (! JWTAuth::parseToken()->authenticate()) {
throw new \Exception('Unable to find the account');
}
} catch (TokenExpiredException $e) {
throw new \Exception('Token has been expired');
} catch (TokenInvalidException $e) {
throw new \Exception('Token is not valid');
} catch (JWTException $e) {
throw new \Exception('Unable to parse the token');
}
return $next($request);
}
}
In your http/kernel.php in
$routeMiddleware
add
'api.auth.validate' => \App\Http\Middleware\ValidateUser::class
Now in your api.php you can use middleware to check it something like:
middleware(['api.auth.validate'])

Exclude route from Slim CSRF middleware

I'm working on a Slim 3 based application with a Twig frontend and I'm also making a REST API.
I've implemented slimphp\Slim-Csrf for the entire app but I now want to exclude this CSRF check from every "API" routes.
I'm trying to implement the "Option 2" of this post :
Slim3 exclude route from CSRF Middleware
Here is the code :
File App\Middleware\CsrfMiddleware.php :
namespace App\Middleware;
class CsrfMiddleware extends \Slim\Csrf\Guard {
public function processRequest($request, $response, $next) {
// Check if this route is in the "Whitelist"
$route = $request->getAttribute('route');
if ($route->getName() == 'token') {
var_dump('! problem HERE, this middleware is executed after the CsrfMiddleware !');
// supposed to SKIP \Slim\Csrf\Guard
return $next($request, $response);
} else {
// supposed to execute \Slim\Csrf\Guard
return $this($request, $response, $next);
}
}
}
File app\app.php :
$app = new \Slim\App([
'settings' => [
'determineRouteBeforeAppMiddleware' => true
]
]);
require('container.php');
require('routes.php');
$app->add($container->csrf);
$app->add('csrf:processRequest');
File app\container.php :
$container['csrf'] = function ($container) {
return new App\Middleware\CsrfMiddleware;
};
File app\routes.php :
<?php
$app->get('/', \App\PagesControllers\LieuController::class.':home')->setName('home');
$app->post('/api/token', \App\ApiControllers\AuthController::class.'postToken')->setName('token');
When I do a POST request on http://localhost/slim3/public/api/token I've got :
Failed CSRF check!string(70) "! problem HERE, this middleware is executed after the CsrfMiddleware !"
Like if my CsrfMiddleware was executed after \Slim\Csrf\Guard...
Anyone has an idea ?
Thank you.
In Slim 3 the middleware is LIFO (last in first out).
Add the middleware in the opposite direction:
Before
$app->add($container->csrf);
$app->add('csrf:processRequest');
After
$app->add('csrf:processRequest');
$app->add($container->csrf);
Notice: The public directory should not be part of the url
Not correct: http://localhost/slim3/public/api/token
Correct: http://localhost/slim3/api/token
To skip the processing within the middleware, just return the $response object.
// supposed to SKIP \Slim\Csrf\Guard
return $response;
Here is how I achieved this with Slim 3.
1) Create a class that extends \Slim\Csrf\Guard as follows.
The CsrfGuardOverride class is key to enabling or disabling CSRF checking for a path. If the current path is whitelist'ed, then the __invoke() method just skips the core CSRF checking, and proceeds by executing the next middleware layer.
If the current path is not in the whitelist (i.e., CSRF should be checked), then the __invoke method defers to its parent \Slim\Csrf\Guard::__invoke() to handle CSRF in the normal manner.
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use \Slim\Csrf\Guard;
class CsrfGuardOverride extends Guard {
/**
* Invoke middleware
*
* #param ServerRequestInterface $request PSR7 request object
* #param ResponseInterface $response PSR7 response object
* #param callable $next Next middleware callable
*
* #return ResponseInterface PSR7 response object
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
{
// Set the name of the route we want whitelisted with a name
// prefix of 'whitelist'. check for that here, and add
// any path to the white list
$route = $request->getAttribute('route');
$routeName = $route->getName();
$whitelisted = strpos($routeName, 'whitelist');
// if url is whitelisted from being CSRF checked, then bypass checking by skipping directly to next middleware
if ($whitelisted !== FALSE) {
return $next($request, $response);
}
return parent::__invoke($request, $response, $next);
}
}
2) Register the CsrfGuardOverride class. Be sure to set settings.determineRouteBeforeAppMiddleware => true as this forces Slim to evaluate routes prior to executing any middleware.
// Method on App Class
protected function configureContainer(ContainerBuilder $builder)
{
parent::configureContainer($builder);
$definitions = [
'settings.displayErrorDetails' => true,
'settings.determineRouteBeforeAppMiddleware' => true,
// Cross-Site Request Forgery protection
\App\Middleware\CsrfGuardOverride::class => function (ContainerInterface $container) {
$guard = new \App\Middleware\CsrfGuardOverride;
$guard->setPersistentTokenMode(true); // allow same CSRF token for multiple ajax calls per session
return $guard;
},
'csrf' => DI\get(\App\Middleware\CsrfGuardOverride::class),
// add others here...
];
$builder->addDefinitions($definitions);
}
3) Add the path that you want to by bypass CSRF checking and give it a name with the prefix 'whitelist':
$app->post('/events/purchase', ['\App\Controllers\PurchaseController', 'purchaseCallback'])->setName('whitelist.events.purchase');

How can I return to a specified URL after login in Laravel?

I'm using Laravel 5.3 and want to return the user to a user-specified URL after login.
I am using a lot of JavaScript and want to return to a specific URL, that isn't the URL the user is trying to access, after they have logged in. The URL is different depending on user action.
For example:
/login?r=/come/here/after/login
I can pass this URL to the login screen, but I can't find a way to pass it through to the auth controller for redirection after login is successful.
In your case I would create a custom auth middleware just for the custom redirected routes:
class PostLoginRedirect
{
public function handle($request, Closure $next, $guard = null)
{
$response = $next($request);
if (\Auth::id() && isset($request->r)) {
// Return the new route redirect.
return redirect($request->r);
}
// Return the custom one in case r? don't exists.
return $response;
}
}
Declare your new middleware on app/Http/Kernel.php
protected $routeMiddleware = [
'login-redirect' => \YourNamespace\PostLoginRedirect::class
];
And add to your routes:
$this->post('login', ['middleware' => 'login-redirect', 'uses' => 'Auth\AuthController#login']);
Maybe you need to do a minor change but must work :)

Laravel cURL POST Throwing TokenMismatchException

I have a problem with POST cURL request to my application.
Currently, I'm building RESTFUL registration function using laravel 5.
The routes for this is example is
localhost:8000/user/create
I pass value using cURL function on terminal
curl -d 'fname=randy&lname=tan&id_location=1&email=randy#randytan.me&password=randytan&remember_token=Y&created_at=2015-03-03' localhost:8000/auth/register/
And this is my routes.php
Route::post('user/create', 'UserController#create');
And this is my function to store the registration user
public function create()
{
//function to create user.
$userAccounts = new User;
$userAccounts->fname = Request::get('fname');
$userAccounts->lname = Request::get('lname');
$userAccounts->id_location = Request::get('id_location');
$userAccounts->email = Request::get('email');
$userAccounts->password = Hash::make(Request::get('password'));
$userAccounts->created_at = Request::get('created_at');
$userAccounts->save();
return Response::json(array(
'error' => false,
'user' => $userAccounts->fname . " " . $userAccounts->lname
), 200);
}
Executing the cURL syntax above, I'm getting this error TokenMismatchException
Do you have any ideas?
Because I'm implementing middleware only in my few urls, and this cURL registration url is not tight into any authentication mechanism.
Thanks before.
In Laravel 5 (latest version) you can specify routes you want to exclude in /app/Http/Middleware/VerifyCsrfToken.php
class VerifyCsrfToken extends BaseVerifier
{
/**
* The URIs that should be excluded from CSRF verification.
*
* #var array
*/
protected $except = [
'rest-api/*', # all routes to rest-api will be excluded.
];
}
Hope this helps.
Laravel 5 enforces CSFR token authentication in middleware by default.
you can disable CSFR on selected route Here is the link
or you can try some of these solutions. Hope so it will help.
changing your csfr token method /app/Http/Middleware/VerifyCsrfToken.php
public function handle ($request, Closure $next)
{
if ( !$request->is("api/*"))
{
return parent::handle($request, $next);
}
return $next($request);
}
In my case, i needed to add the route on api.php instead of web.php

Categories