Simple role-based authentication in Laravel

A common thing I see in Laravel communities such as the Laracasts discussion forums is developers who want multiple user types in their application, decide to then create multiple “user” models and tables, and then find they have issues with things like authorisation. My advice in 99% of these scenarios is: don’t create multiple user models and tables. A user is a user. Use roles to determine what a user can and cannot do in your application.

Using roles can sound scary and complicated but it doesn’t have to be. It can be something as simple as a string with the name of a role (i.e. “admin” or “manager”). You can also easily add single role and multiple roles to users in your Laravel applications. Below are two simple approaches to both single roles and multiple roles.

Single roles

If a user should only have a single role in your application then this is the most straightforward approach. You can add a role column to your users table, and then check the value of the column when you need to check a user’s role.

First, create a migration that adds the column to your users table:

Migration

php artisan make:migration add_role_column_to_users_table --table=users
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::table('user', function (Blueprint $table) {
            $table->string('role')->nullable();
        });
    }

    public function down()
    {
        Schema::table('user', function (Blueprint $table) {
            $table->dropColumn('role');
        });
    }
}

Middleware

To then check the role, you can use middleware. We can use a middleware parameter to specify the role to check for. In fact, checking for a role is actually the example in the Laravel docs!

Create middleware

php artisan make:middleware EnsureUserHasRole
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class EnsureUserHasRole
{
    public function handle(Request $request, Closure $next, string $role)
    {
        if ($request->user()->role === $role) {
            return $next($request);
        }

        abort(403);
    }
}

Tip: Any time you’re dealing with a Request object you can get the authenticated user without resorting to the Auth facade or auth()->user() helper. Just call $request->user() and that will return the authenticated user. You can do this in controllers as well.

Register the middleware

Because the middleware class has a parameter, we’ll need to register it in your app/Http/Kernel.php file:

protected $routeMiddleware = [
    'role' => \App\Http\Middleware\EnsureUserHasRole::class,
    // Other route middleware...
];

You can then apply the middleware to a route or a route group:

Route::middleware(['auth', 'role:admin'])->group(function () {
    // User is authentication and has admin role
});

Be sure to include the auth middleware before the role middleware so that the user is authenticated. The user will need to be authenticated first before the role middleware can check the user’s role.

Multiple roles

If in your application users can have multiple roles, rather than a single role, then you’ll need to extend your models a bit.

One approach is to create a Role model that contains the name of roles, and then define a many-to-many relationship between your Role and User models.

Models and migrations

First, create the Role model and its corresponding migration:

php artisan make:model Role -m

Then fill out the role table migration:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('roles', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('roles');
    }
}

Now create a pivot table to link roles to users. Following Laravel’s naming conventions, the table would be named role_user:

php artisan make:migration create_role_user_table --create=role_user
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('role_user', function (Blueprint $table) {
            $table->primary(['role_id', 'user_id']);
            $table->foreignId('role_id')->constrained()->cascadeOnDelete();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        });
    }

    public function down()
    {
        Schema::dropIfExists('role_user');
    }
}

You can now set up the corresponding relationships in the two models:

class Role extends Model
{
    public function users()
    {
        return $this->belongsToMany(User::class);
    }
}
class User extends Authenticatable
{
    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }
}

Middleware

This will make the middleware more complicated though, as we now have to query a relation, rather than simply read the value of a column. But the process is the same: check for a role and alow the request if the user does have the role, or return a 403 Forbidden error response if they do not.

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class EnsureUserHasRole
{
    public function handle(Request $request, Closure $next, string $role)
    {
        if ($request->user()->roles()->where('name', '=', $role)->exists()) {
            return $next($request);
        }

        abort(403);
    }
}

The usage is exactly the sample (add role:foo to a route or route group), but it will now query the roles relation instead.

Conclusion

So, next time you’re dealing with an application with multiple user types, consider using roles instead of splitting your users across multiple models and tables. It’ll make users in your application much easier to deal with!

There are, of course, some scenarios where users may need to be segregated for legal or regulatory reasons, but for 99% of cases where you just want different “types”, the above will suffice.