Create SPA with Laravel and Vue - Part 3 (Securing backend with JWT)

4 June 2018
In this post we are going to secure our freshly baked backend. I was thinking about Laravel Passport but it would be an overkill in this case as we won't be using multiple external apps to consume our API. Instead we are going to use  jwt-auth package.

First, install package via Composer:
composer require tymon/jwt-auth 1.0.0-rc2

Now publish package config file:
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

Next generate JWT secret that we will use to create tokens:
php artisan jwt:secret

Now we need to add a few things to our .env file. Open it and you should see JWT_SECRET at the bottom. Now type:
JWT_ALGO=RS256
JWT_PUBLIC_KEY=jwt/public.pem
JWT_PRIVATE_KEY=jwt/private.pem
JWT_PASSPHRASE=afshs2o32323savfba
Tokens will be signed using public & private keys with RS256 asymmetric algorithm. We are also setting passphrase for our private key (please change that to your own random string).

Now we need to modify config/jwt.php:
 'keys' => [

        /*
        |--------------------------------------------------------------------------
        | Public Key
        |--------------------------------------------------------------------------
        |
        | A path or resource to your public key.
        |
        | E.g. 'file://path/to/public/key'
        |
        */

        'public' => file_get_contents(public_path(env('JWT_PUBLIC_KEY'))),

        /*
        |--------------------------------------------------------------------------
        | Private Key
        |--------------------------------------------------------------------------
        |
        | A path or resource to your private key.
        |
        | E.g. 'file://path/to/private/key'
        |
        */

        'private' => file_get_contents(storage_path(env('JWT_PRIVATE_KEY'))),
We are setting location for public (public/jwt/) and private (storage/jwt/) key. 

Now we need to generate keys using previousily typed JWT_PASSPHRASE:
openssl genrsa -passout pass:afshs2o32323savfba -out storage/jwt/private.pem -aes256 4096
openssl rsa -passin pass:afshs2o32323savfba -pubout -in storage/jwt/private.pem -out public/jwt/public.pem

Next we will modify our User model (app/User.php):
namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;
use \Illuminate\Http\Request;

class User extends Authenticatable implements JWTSubject
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'username', 'password'
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier() {
        return $this->getKey(); // Eloquent Model method
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
    */
    public function getJWTCustomClaims() {
        return ['ip' => request()->ip()];
    }
}
We are adding IP address as additional security (will be checked in frontend).

Now we'll create AuthController.php:
namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Facades\JWTAuth;
use \Illuminate\Http\Request;

class AuthController extends Controller
{
    public function login(Request $request)
    {
        $credentials = $request->only('username', 'password');
        $customClaims = [];
        try {
            if (!$token = JWTAuth::attempt($credentials, $customClaims)) { 
                return response()->json(['error' => 'invalid_credentials'], 401);
            }
        } 
        catch (JWTException $e) {
            return response()->json(['error' => 'could_not_create_token'], 500);
        }
        return response()->json(['token' => $token]);
    }

    public function logout()
    {
        JWTAuth::invalidate(JWTAuth::getToken());
        return response()->json(['status' => 'Logged out successfully']);
    }

    public function getIP()
    {
        return request()->ip();
    }
}

Create new middleware (Middleware/RefreshToken.php):
namespace App\Http\Middleware;

use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
use Tymon\JWTAuth\Exceptions\JWTException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;

class RefreshToken extends BaseMiddleware {

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, \Closure $next) {

        $this->checkForToken($request);

        try {
            if (!$this->auth->parseToken()->authenticate()) {
                throw new UnauthorizedHttpException('jwt-auth', 'User not found');
            }
            $payload = $this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray();
            return $next($request);
        } catch (TokenExpiredException $t) {
            $payload = $this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray();
            $key = 'block_refresh_token_for_user_' . $payload['sub'];
            $cachedBefore = (int) Cache::has($key);
            if ($cachedBefore) {
                \Auth::onceUsingId($payload['sub']);
                return $next($request);
            }
            try {
                $newtoken = $this->auth->refresh();
                $gracePeriod = $this->auth->manager()->getBlacklist()->getGracePeriod();
                $expiresAt = Carbon::now()->addSeconds($gracePeriod);
                Cache::put($key, $newtoken, $expiresAt);
            } catch (JWTException $e) {
                throw new UnauthorizedHttpException('jwt-auth', $e->getMessage(), $e, $e->getCode());
            }
        }

        $response = $next($request); // Token refreshed and continue.

        return $this->setAuthenticationHeader($response, $newtoken);
    }

}

Add new middleware to Kernel.php:
    protected $routeMiddleware = [
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'jwt' => \App\Http\Middleware\RefreshToken::class
    ];

Secure our PageController with middleware:
public function __construct()
{
	$this->middleware(['jwt'])->except(['show', 'getNav']);
}

Finally, add following routes to routes/api.php:
Route::post('login', 'AuthController@login');
Route::post('logout', 'AuthController@logout');
Route::get('get-ip', 'AuthController@getIP');

And you are ready to go! Let's test it out with Advanced Rest Client.

Login:



Get pages without token:



Get pages with token:



That's it folks!

Next, we will start working on our frontend.
2018 | korek.tech
Made with and SSR