How To Handle Authorization In Laravel The Right Way

How To Handle Authorization In Laravel The Right Way

Given Ncube

Laravel's authorization module gives you the skeleton to implement robust authorization logic in your app, with two choices: Gates and Policies.

Spatie Laravel-Permission offers a way to implement roles and permissions in your app.

Both are fully documented with examples, but when you try to apply it, everything just gets messy.

Here's how I implement authorization in my Laravel projects.

Background and preconditions

For this technique/pattern or whatever you want to call it to work. I follow a set of conventions to make it easier.

1. Models

I don't use modules/services/repositories or any other shenanigans coming up every day. All my models are in the default app/models directory, and they all extend Eloquent's Model.

2. Routes

For routing whenever possible I use resource routes. For example, say I'm building an e-commerce store.

Route::resource('products', ProductController::class);

3. Controllers

My controllers are straight defaults from Laravel and all live in the app/controllers directory. I might namespace them depending on what they are for. And again, I use resourceful controllers.

Just stick to the defaults.

The logic behind how I authorize goes like this:

For every app, there's are going to be models, CRUD and users. Each CRUD action performed on a model has to be authorized by a single permission. For example, to create a product, a user should have permission to create products.

Laravel offers a nice way to do just that, with polices. Policies authorize the whole CRUD operations and pairs nicely with resourceful controllers, as you'll see by the end of this article.

Each method should return a boolean to see if a user can perform a certain CRUD action, We then just check if the user has permission to that and return that boolean. Easy-peasy.

Now, without enough theory, let's see how this is done.

Requirements

To implement this, you are going to need the following

  • A Laravel app, best if it's a complete app that just needs authorization

  • spatie/laravel-permissions To handle roles and permissions

Setup

Let's install the required packages. In your terminal, install spatie/laravel-permissions with composer.

composer require spatie/laravel-permissions

Follow the documentation to set up the package.

Now we are ready to do the authorization. But first we need to…

Open the Gates for super admin

For every app, there has to be a super admin or root user with all the rights and can do anything. This is the first thing I would implement. To do this, we need to use Gate::before() method to allow a user to do anything as long as they have the super admin role.

In your AuthServiceProvider add this to the boot method after registering polices.

Gate::before(function ($user, $ability) {  
    return $user->hasRole('super admin') ? true : null;  
});

This will make sure that every time $user->can() is called, it returns true as long as the user has super admin role.

Generate roles and permissions

Now that's done, let's go ahead and create the super admin role. But first we need more preparation. See, normally you'd add authorization after your application is finished, and you know everything works as it's supposed to. At least, that's how I'd do it.

To make sure we can just plug and play this into production, let's create a seeder.

php artisan make:migration RolesAndPermissions

In the seeder I will use the following code

use Illuminate\Database\Eloquent\Model;  
use Illuminate\Database\Seeder;  
use Illuminate\Support\Facades\File;  
use Illuminate\Support\Str;  
use ReflectionClass;  
use Spatie\Permission\Models\Role;  
use Spatie\Permission\Models\Permission;  
use Symfony\Component\Finder\SplFileInfo;

class RolesPermissions extends Seeder  
{  
    /**  
     * Run the database seeds.     
     *     
     * @return void  
     */  
    public function run()  
    {  
        $roles = collect([  
            'super admin',  
        ]);  
  
        $models = collect(File::allFiles(app_path()))  
            ->map(function (SplFileInfo $info) {  
                $path = $info->getRelativePathname();  
  
                $class = sprintf('\%s%s',  
                app()->getNamespace(),  
                Str::replace('/', '\\', Str::beforeLast($path, '.')));  
  
                return $class;  
  
            })  
            ->filter(function (string $class) {  
                try {  
                    $reflection = new ReflectionClass($class);  
                } catch (\ReflectionException $throwable) {  
                    return false;  
                }  
  
                return $reflection->isSubclassOf(Model::class) && 
	            !$reflection >isAbstract();  
            })  
            ->map(function ($model) {  
                return Str::lower(Str::plural(Str::afterLast($model, '\\')));  
            })  
            ->map(function ($model) {  
                return [  
                    "create {$model}",  
                    "update {$model}",  
                    "delete {$model}",  
                    "restore {$model}",  
                    "view {$model}",  
                    "force delete {$model}",  
                ];  
            });  
  
  
        $roles->each(function ($role) use ($models) {  
            $role = Role::create(['name' => $role]);  
        });  
    }  
}

Okay, let me explain…

To authorize this, we just require permissions for each CRUD action per model. For example, there is a permission to create users or update users which we can use as $user->can('update users')

Notice there is only one role super admin defined. All other roles will be created and assigned permissions via a GUI by the super admin.

In the first part of the code, I get all the classes in the app directory as their fully qualified class names, e.g., \App\Models\User.

Next, we use the Reflection API to make sure the class is insatiable, and it extends Eloquent's base Model.

Next, I map that and return just a plural lower case model name like users. Map that again to return an array of permission for each model like create users, view users, update users, or delete users for example.

Then we just go on to create the default roles which is just super admin, but you're welcome to add more if you want like editor, seo consultant, intern or something.

Let's go ahead, seed the database

php artisan db:seed RolesAndPermissions

Now let's create the policies for each of our models we have.

Defining the policies

Let's take for example we are building an e-commerce store, we're going to generate the policy for the Product model

php artisan make:policy ProductPolicy --model=Product

This will generate a boilerplate policy in your app/policies directory. This is how I would define my policy.

namespace App\Policies;  
  
use App\Models\Product;  
use App\Models\User;  
use Illuminate\Auth\Access\HandlesAuthorization;  
  
class ProductPolicy  
{  
    use HandlesAuthorization;  
  
    /**  
     * Determine whether the user can view any models.     
     *     
     * @param \App\Models\User  $user  
     */  
    public function viewAny(User $user): bool  
    {  
        return $user->can('view all products');  
    }  
  
    /**  
     * Determine whether the user can view the model.     
     *     
     * @param \App\Models\User  $user  
     * @param \App\Models\Product  $product  
     */  
    public function view(User $user, Product $product): bool  
    {  
        return $user->can('view products');  
    }  
  
    /**  
     * Determine whether the user can create models.     
     *     
     * @param \App\Models\User  $user  
     */  
    public function create(User $user): bool  
    {  
        return $user->can('create products');  
    }  
  
    /**  
     * Determine whether the user can update the model.     
     *     
     * @param \App\Models\User  $user  
     * @param \App\Models\Product  $product  
     */  
    public function update(User $user, Product $product): bool  
    {  
        return $user->can('update products');  
    }  
  
    /**  
     * Determine whether the user can delete the model.     
     *     
     * @param \App\Models\User  $user  
     * @param \App\Models\Product  $product  
     */  
    public function delete(User $user, Product $product): bool  
    {  
        return $user->can('delete products');  
    }  
  
    /**  
     * Determine whether the user can restore the model.     
     *     
     * @param \App\Models\User  $user  
     * @param \App\Models\Product  $product  
     */  
    public function restore(User $user, Product $product): bool  
    {  
        return $user->can('restore products');  
    }  
  
    /**  
     * Determine whether the user can permanently delete the model.     
     *     
     * @param \App\Models\User  $user  
     * @param \App\Models\Product  $product  
     */  
    public function forceDelete(User $user, Product $product): bool  
    {  
        return $user->can('force delete products');  
    }  
}

Take for example the update() method, I just check if the user can update products. This will return true if the user has permission to update products otherwise it returns false.

Authorizing controllers

For each CRUD authorization method, we just check if the user has permission to perform that action. Since we are using resourceful controllers. Our product controller can look like this.

/**  
 * Create the controller instance. 
 * 
 * @return void  
 */  
public function __construct()  
{  
    $this->authorizeResource(Product::class, 'product');  
}

Calling the authorizeResource() method will authorize all the resourceful routes using that Product Policy we just created, simple and powerful.

Just like that you have a robust authorization logic with strong role based access control. At this point I would typically give a set of permissions to a role and give that role to a user. And with that the user gets the permission. I would use a GUI so that I can change permissions any time.

Authorization in just one artisan command

So as I was writing this article, I realized that I've been re implementing this over and over again. I decided to create a package that helps you generate all that in just one command.

How to use this package

Getting started is simple. You just need to install the package via composer

composer require flixtechs-labs/laravel-authorizer

Next run the following command to setup the package

php artisan authorizer:setup

After that generate your supercharged policy with

php artisan authorizer:policies:generate Product --model=Product

Go to the documentation to see how this package can speed up your development time.

Final thoughts

Handling authorization in Laravel can be quite overwhelming especially when you're starting out. I hope this article gives you a great starting to start using authorization in your Laravel apps.