In the last post we have managed to login via Passport and return a response containing the access token. This way we need to store the access token on client side and send it attached to every request in order to access the protected routes. As you’ve may already heard, storing sensitive data on client side could be a security issue. There are ways to protect user’s data but we will go for a safer way (in my opinion) and store the access token inside a cookie. This cookie is passed on every request/response between client and server and cannot be altered by client.
We’ll try to implement what Laravel documentation describes here: https://laravel.com/docs/5.7/passport#consuming-your-api-with-javascript
Due the fact that I felt it is not even close to want it promises I want to share this article with you to understand better what is happening under the hood, or just save you few hour of investigating what is wrong with that.
For some security improvements I highly recommend adding the following link into .env file: SESSION_SECURE_COOKIE=true
. This will allow cookies to be set only over HTTPS connection. This will protect us from any man-in-the middle attack.
Set it ONLY in production, when you load your website over HTTPS.
And in config/session.php
ensure that same_site
is set to "strict"
in order to disable cross origin requests.
To protect against CSRF we will use Laravel’s csrf-token, but we’ll talk about this a bit later.
Inside app\Http\Kernel.php
insert following lines:
'api' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
...
],
CreateFreshApiToken middleware will generate a JWT access token, create a cookie with it + CSRF token of current session + an expiration date and add it to the response.
EncryptCookies will ensure that any incoming or outgoing cookies will be encrypted so the client cannot see the actual value of it.
AddQueuedCookiesToResponse will attach the cookie to our response.
StartSession will give us a session based on the cookie. This way we can access user’s data from it.
Make sure all 4 classes are inserted above: 'throttle:60,1'
and 'bindings'
.
If you take a look inside CreateFreshApiToken class, you will find the following method:
protected function requestShouldReceiveFreshToken($request)
{
return $request->isMethod('GET') && $request->user($this->guard);
}
Until now I haven’t find a good reason for checking if the request method is “GET” (Later edit: I’ve found one. Maybe will discuss it in a further post) We need to give the user a valid cookie as soon as he logs in. And for oblivious reasons the login and register methods should always be a “POST”. So I created a CustomCreateApiToken class and override the method as follows:
use Laravel\Passport\Http\Middleware\CreateFreshApiToken as Middleware;
class CustomCreateApiToken extends Middleware
{
/**
* Determine if the request should receive a fresh token.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
protected function requestShouldReceiveFreshToken($request)
{
return $request->user($this->guard);
}
}
$api->version('v1', function ($api) {
$api->group(['middleware' => 'api'], function ($api) {
$api->post("register", 'App\Http\Controllers\Api\V1\Auth\RegisterController@register');
$api->get("register/{token}", 'App\Http\Controllers\Api\V1\Auth\RegisterController@registerActivate');
$api->post("login", 'App\Http\Controllers\Api\V1\Auth\LoginController@login');
...
});
// Protected routes
$api->group(['middleware' => 'auth:api'], function ($api) {
$api->get('profile', 'App\Http\Controllers\Api\V1\ProfileController@show');
$api->get('logout', 'App\Http\Controllers\Api\V1\Auth\LoginController@logout');
});
});
As you can see above, we added the login and register routes in a group protected by api middleware in order to call CreateFreshApiToken after the user login.
Profile and logout routes are set under auth:api
to be protected by Passport API authentication.
Cookie’s lifetime can be set in config\session.php
:
'lifetime' => env('SESSION_LIFETIME', 120), // minutes
If the cookie expires, our client application should be noticed about that in a nice manner. At this moment Passport returns a JSON like this:
{"message":"Unauthenticated.","status_code":500}
It’s not the best message you can receive. A 500 error code is usually returned for a server error but in our case there is an Authorization error which is usually reported as 401. So I created a Authenticate class, override the authenticate method and catch that 500 response and forwarded a 401 response code with a custom message:
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Exception;
class Authenticate extends Middleware
{
/**
* Determine if the user is logged in to any of the given guards.
*
* @param \Illuminate\Http\Request $request
* @param array $guards
* @return void
*
* @throws \Illuminate\Auth\AuthenticationException
*/
protected function authenticate($request, array $guards)
{
try {
parent::authenticate($request, $guards);
} catch (Exception $e) {
abort(401, 'Unauthorized action.');
}
}
}
To be sure it is active you shall check the app\Http\Kernel.php
file to have the right route to your class:
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
...
];
You can craft your own cookie session handling.
If you are implementing this in a low risk app you can set a forever cookie. This way, the user will never be logged out.
If you are working on something big (eg. a bank software) you can set a low lifetime, let’s say 15 mins and after this user should login again.
You can find the updated code here: https://github.com/danielcrt/laravel5.7-passport-dingo-api-boilerplate
If you have any questions or improvements please let us know in comments section.