JWT HttpOnly authorization with Laravel and React
Vulnerabilities
Most of the time I use the library called Jwt Auth for authenticating in Laravel projects. The process is quite simple: send the login credentials to the API, it gives back a token which gets stored - most of the time in local storage -, then place that token into the header of each API call. There is one major issue with that solution: if you have a compromised script served from a CDN or if there is malicious code in one of your JavaScript libraries, they can steal the token out of local storage. So storing the token in a place where JavaScript can access it is a bad practice. HttpOnly cookie is a more secure place to put the token since no js code can access it. Fortunately, Laravel JW Auth library let you do that out of the box. It has multiple ways to authenticate logged-in users, looking for a token in cookies is one of them. On the other hand, cookie-based authentication has a low point too, it makes CSRF attacks possible. In order to overcome that vulnerability, we need to send unique tokens in the request header. Let’s see how it works with an example.
Implementation
Let’s start with an empty Laravel (I used v5.8.35 in this tutorial). You also need two php libraries: jwt-auth and laravel-cors. On their websites you will find info on how to install them.
First, a route must be implemented for login endpoint:
<?php
use Illuminate\Http\Request;
Route::middleware(['api'])->group(function () {
Route::namespace('Auth')
->prefix('auth')
->name('auth.')
->group(function () {
Route::post(
'login',
'AuthController@login'
)->name('login');
});
});Then we need a controller for that route:
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Http\Resources\AuthResource;
use Illuminate\Http\JsonResponse;
class AuthController extends Controller
{
/**
* Return authenticated user.
*
* @param LoginRequest $request
* @return JsonResponse
*/
public function login(LoginRequest $request): JsonResponse
{
return (new AuthResource(auth()->user()))
->response()
->withCookie(
'token',
auth()->getToken()->get(),
config('jwt.ttl'),
'/'
);
}
}There are two classes that are not part of the Laravel library, one of them is LoginRequest which is responsible to authenticate the user by the given credentials and assign a csrf token for the user.
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
$credentials = $this->only(
'data.email',
'data.password'
)['data'];
return auth()
->claims(['csrf-token' => str_random(32)])
->attempt($credentials);
}
/**
* Define validation rules.
*
* @return array
*/
public function rules(): array
{
return [];
}
}The other one is AuthResource and it generates a json response for this endpoint, you can read more about Laravel’s API resources here.
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class AuthResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'csrf_token' => auth()->payload()->get('csrf-token'),
];
}
}At this point you should be able to execute a login request successfully and get back a cookie with jwt token and a csrf token in the json response, we’re going to need both of them. We have no issue with jwt token other than tell the ajax call to send cookies automatically (e.g. you have to set withCredentials property for axios). The request header must include the csrf token, if you use axios you need something like this:
axios.defaults.headers.common['CSRF-Token'] = 'My CSRF token';The jwt token will be validated automatically by the library but we need to take care of CSRF token validation.
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Access\AuthorizationException;
use Closure;
class CsrfMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if (
$request->header('csrf-token') !==
auth()->payload()->get('csrf-token')
) {
throw new AuthorizationException;
}
return $next($request);
}
}Also, CsrfMiddleware needs to be added to the application’s route middleware list (app/Http/Kernel.php):
protected $routeMiddleware = [
... 'csrf' => \App\Http\Middleware\CsrfMiddleware::class,
...
];
Now every route you protect with ‘auth’ and ‘csrf’ middleware must receive these tokens.
One important note: the front- and back-end must be on the same domain to make this work (they can have different subdomain thought).
I hope this short tutorial will help you build an HttpOnly cookie based authentication.
