Role Based Authentication in Laravel with JWT

From personal experience, no JWT (JSON Web Token) library incorporates a feature for role-based authentication, at least for my core languages which are Node, PHP, C# and Java. In as much as the trend is building stateless API applications, only session authentication libraries come with role authorization helpers.

I am going to show you a simple hack for adding role based authentication to JWT powered applications, using Laravel as case study.

Background

You can skip this section if you know everything about JWT and role-based authentication.

JWT is the best option at this time to create authentication for stateless applications, mostly Web API. It has a lot of advantages including flexibility, enables scaling of HTTP applications, self contained and available in all standard programming languages.

In Laravel, we are going to use Tymon's jwt-auth as demonstrated in this tutorial.

Role based authentication on the other hand is authorization mechanisms for applications. It does not just end at collecting username/email or password but figuring out identity and assigning roles to these identities while restricting permissions too.

A user is an entity and has different characteristics from another. She can create another entity but might not be allowed to delete the entity. She might be allowed to view some entity but not allowed to edit it.

Determining these restrictions and permissions is where the headache is because JWT already handles authenticating the user. We will make use of a common library, Entrust, to achieve authorization.

Where the problem lies as already described in the introduction is working with JWT and authorization. This might not be a problem to professionals but I am 90% certain that beginners are battling with this judging from questions thrown around in the community. Let us try and tackle this problem.

Laravel's HTTP Middleware

Middleware seem to scare beginners a lot and trust me I had that experience when I was new to Node, specifically Express JS. When I finally could wrap my mind around the concept, it felt so easy to do all over again and again. However, explaining middleware is not in the scope of this article.

For now, just see authentication middleware as the automatic security gateways in places like the airport but in this case way smarter.

We will dig into the source of jwt-auth and Entrust to see how their middlwares were developed and how we can blend them to work together. That sounds scary, I know, but it is no big deal.

Sample Application

To get a clearer picture we will make a sample app to see how we can create custom middleware that will tie the functionalities of Entrust and JWT. What we need to do first is setup a Laravel application.

Setup and Dependencies

After we install and create a Laravel application we can pull in the the following dependencies in composer.json:

"tymon/jwt-auth": "0.5.*",
"zizaco/entrust": "dev-laravel-5"

We need to install the new dependencies by running:

composer update

Next, let us include the new installed dependencies to our config/app.php. In the providers array, add:

$providers = [
// Other providers
Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class,
Zizaco\Entrust\EntrustServiceProvider::class
]

and in the aliases array:

$aliases = [
// Other aliases
'JWTAuth'   => Tymon\JWTAuth\Facades\JWTAuth::class,
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class,
'Entrust' => Zizaco\Entrust\EntrustServiceProvider::class
]

Then publish the JWT dependency with the following commands:

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\JWTAuthServiceProvider"

We need to generate a secret key for the JWT. Run the following command to do so:

php artisan jwt:generate

Another important thing to do is generate the migrations for the roles and permissions with there relationships:

php artisan entrust:migration

We can go ahead and create a seed for the users table. We will not create seeds for roles and permission entities so we can demonstrate how to do that from our controller logic. In database/seeds/DatabaseSeeder.php, update the run() method with:

   public function run()
    {
        Model::unguard();

        // $this->call(UserTableSeeder::class);
        DB::table('users')->delete();

        $users = array(
            ['name' => 'Ryan Chenkie', 'email' => 'ryanchenkie@gmail.com', 'password' => Hash::make('secret')],
            ['name' => 'Chris Sevilleja', 'email' => 'chris@scotch.io', 'password' => Hash::make('secret')],
            ['name' => 'Holly Lloyd', 'email' => 'holly@scotch.io', 'password' => Hash::make('secret')],
            ['name' => 'Adnan Kukic', 'email' => 'adnan@scotch.io', 'password' => Hash::make('secret')],
        );

        // Loop through each user above and create the record for them in the database
        foreach ($users as $user)
        {
            User::create($user);
        }

        Model::reguard();
    }

The final step in the setup section is to run migration hoping that you have configured your database in .env:

php artisan migrate

The Needed Routes, Controller and Eloquent Models

The following routes will be needed for us to accomplish our task in this tutorial:

// Route to create a new role
Route::post('role', 'JwtAuthenticateController@createRole');
// Route to create a new permission
Route::post('permission', 'JwtAuthenticateController@createPermission');
// Route to assign role to user
Route::post('assign-role', 'JwtAuthenticateController@assignRole');
// Route to attache permission to a role
Route::post('attach-permission', 'JwtAuthenticateController@attachPermission');

// API route group that we need to protect
Route::group(['prefix' => 'api', 'middleware' => ['ability:admin,create-users']], function()
{
    // Protected route
    Route::get('users', 'JwtAuthenticateController@index');
});

// Authentication route
Route::post('authenticate', 'JwtAuthenticateController@authenticate');

Each route points to respective action methods under the JwtAuthenticateController so it is time to create that:

<?php

namespace App\Http\Controllers;

use App\Permission;
use App\Role;
use App\User;
use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
use Log;

class JwtAuthenticateController extends Controller
{

    public function index()
    {
        return response()->json(['auth'=>Auth::user(), 'users'=>User::all()]);
    }

    public function authenticate(Request $request)
    {
        $credentials = $request->only('email', 'password');

        try {
            // verify the credentials and create a token for the user
            if (! $token = JWTAuth::attempt($credentials)) {
                return response()->json(['error' => 'invalid_credentials'], 401);
            }
        } catch (JWTException $e) {
            // something went wrong
            return response()->json(['error' => 'could_not_create_token'], 500);
        }

        // if no errors are encountered we can return a JWT
        return response()->json(compact('token'));
    }

    public function createRole(Request $request){
        // Todo       
    }

    public function createPermission(Request $request){
        // Todo       
    }

    public function assignRole(Request $request){
         // Todo
    }

    public function attachPermission(Request $request){
        // Todo       
    }

}

We have only implemented two of the few methods. The index() method is for our protected route, which just lists all users. The authenticate() method uses the JWTAuth's attempt() method to create a token for the user.

use App\Permission;
use App\Role;
use App\User;

You are obviously wondering when we created the Permission and Role classes as seen above. We have yet to do that, so in your app directory, create a Permission.php file:

<?php namespace App;

use Zizaco\Entrust\EntrustPermission;

class Permission extends EntrustPermission
{
}

and Roles.php:

<?php namespace App;

use Zizaco\Entrust\EntrustRole;

class Role extends EntrustRole
{
}

Those are just the eloquent models needed for the permissions and roles table.

We need a way to tell the User model that it has some kind of relationship with roles and permissions:

<?php

use Zizaco\Entrust\Traits\EntrustUserTrait;

class User extends Eloquent
{
    use Authenticatable, CanResetPassword, EntrustUserTrait;
    ...
}

Notice we have replaced the Authorizable with EntrustUserTrait trait in the model as we are not going to use Laravel's built in authorization as it will cause conflicts with Entrust.

Controller Action Methods for Entrust

Remember we left some of the controller action methods empty, now is the time to flesh them out. The actions are expected to create roles, permissions, attach permissions to roles and assign roles to users

The createRole method only requires a name input which is the going to be set as the name of the role:

public function createRole(Request $request){

        $role = new Role();
        $role->name = $request->input('name');
        $role->save();

        return response()->json("created");

    }

NB: This is just a demo. In a real application, kindly do some protective checks before saving. Also visit the Entrust documentation for more available options.

createPermission is just like the createRole method, but creates a permission:


    public function createPermission(Request $request){

        $viewUsers = new Permission();
        $viewUsers->name = $request->input('name');
        $viewUsers->save();

        return response()->json("created");

    }

Next, the assignRole is responsible for assigning a given role to a user. Which means it needs a role id and a user object:

 public function assignRole(Request $request){
        $user = User::where('email', '=', $request->input('email'))->first();

        $role = Role::where('name', '=', $request->input('role'))->first();
        //$user->attachRole($request->input('role'));
        $user->roles()->attach($role->id);

        return response()->json("created");
    }

Finally, the attachPermission adds the permissions for a role:

public function attachPermission(Request $request){
        $role = Role::where('name', '=', $request->input('role'))->first();
        $permission = Permission::where('name', '=', $request->input('name'))->first();
        $role->attachPermission($permission);

        return response()->json("created");
    }

The Hack

Take another look at the routes. You will see that there is middleware called ability on the protected routes:

Route::group(['prefix' => 'api', 'middleware' => ['ability:admin,create-users']], function()
{
        Route::get('users', 'JwtAuthenticateController@index');

});

We are just saying that we need the user to be an admin or have the create-users permissions before they can access the routes in this group.

Use Postman to create an admin role and create-users permission.

Doing that will not protect the routes but rather throw errors, so we need to define this middleware. Entrust already has a EntrutAbility that can be seen here but the problem is that it works with sessions and not tokens.

What we can do is extend the JWT's middleware to include Entrust's and work with a token, not session.

Run:

php artisan make:middleware TokenEntrustAbility

TokenEntrustAbility will be created in app/Http/Middleware. Replace the content with:

<?php

namespace App\Http\Middleware;

use Closure;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Tymon\JWTAuth\Middleware\BaseMiddleware;

class TokenEntrustAbility extends BaseMiddleware
{
       public function handle($request, Closure $next, $roles, $permissions, $validateAll = false)
    {

        if (! $token = $this->auth->setRequest($request)->getToken()) {
            return $this->respond('tymon.jwt.absent', 'token_not_provided', 400);
        }

        try {
            $user = $this->auth->authenticate($token);
        } catch (TokenExpiredException $e) {
            return $this->respond('tymon.jwt.expired', 'token_expired', $e->getStatusCode(), [$e]);
        } catch (JWTException $e) {
            return $this->respond('tymon.jwt.invalid', 'token_invalid', $e->getStatusCode(), [$e]);
        }

        if (! $user) {
            return $this->respond('tymon.jwt.user_not_found', 'user_not_found', 404);
        }

       if (!$request->user()->ability(explode('|', $roles), explode('|', $permissions), array('validate_all' => $validateAll))) {
            return $this->respond('tymon.jwt.invalid', 'token_invalid', 401, 'Unauthorized');
        }

        $this->events->fire('tymon.jwt.valid', $user);

        return $next($request);
    }
}

Now the JWT middleware has been extended to include the Entrust's ability middleware by including this tiny block:

    if (!$request->user()->ability(explode('|', $roles), explode('|', $permissions), array('validate_all' => $validateAll))) {

            return $this->respond('tymon.jwt.invalid', 'token_invalid', 401, 'Unauthorized');

        }

Finally, register the middleware in app/Http/Kernel.php:

protected $routeMiddleware = [
// ... Other middlewares
'ability' => 'App\Http\Middleware\TokenEntrustAbility'
]

Now you won't get an exception again if you tried accessing the protected routes. Instead you will get a 401 or the data you seek if you are authenticated as the required role.

Conclusion

As a recap, we just allowed jwt-auth and Entrust to work independently. Then we went an extra mile to create a custom middleware other than what the libraries provide to better meet our needs.

Use the provided demo as an example if you got lost anywhere in the middle of this tutorial. Feel free to use the comments as a mini forum for your confusions and we will be glad to fight your code battles with you.

Chris Nwamba

Passion for instructing computers and understanding its language. Would love to remain a software engineer in my next life.