How to Implement Role-Based Access Control (RBAC) in Laravel
- Share:
So, you want to implement RBAC in your Laravel application.
Before we dive into how let’s talk about why. You already have an application, and naturally, your users don’t want everyone having access to their data. You also need to ensure that certain users can’t do certain things—especially anything destructive like deleting things.
As your application starts to scale, the need for permissions in your application not only gets more obvious - but managing these permissions also starts to become a task on its own. You might end up with a massive spreadsheet just to figure out who can (or can’t) do what.
It’s one thing if you have 10 users. Sure, it’s annoying to manage, but it doesn’t happen all that often. But once you have 100 or more, it shifts from a task you don’t want to do, to one you can’t possibly handle on your own.
This is where RBAC comes into play. The idea of permissions being templated to a specific role, so that you can apply these templates to users. A user with the role of editor, inherits a certain set of permissions. Whereas a user with the role of admin, inherits a different set of permissions. There will of course be overlap between these roles, but in general different roles will be built around different actions and abilities within your application.
What this guide will cover:
- An overview of RBAC and its importance
- Basic authorization in Laravel (Gates and Policies)
- Moving from simple permission checks to a more structured approach
- Leveraging external solutions like Permit and a Policy Decision Point (PDP)
- Practical implementation tips and code examples
What Actually is RBAC?
RBAC stands for Role Based Access Control. It is a method of restricting access to resources, based on the role of the user within the system. There are many ways in which you can implement an RBAC system into your application, and out of the box Laravel has a somewhat simple implementation that you are able to leverage already. The main benefit of using RBAC over a flat permissions based system, is mostly the ease of management of permissions themselves. As I already said, you start to template “actors” within your application and what they are able to do. You then apply this to a user, which limits what they can do through their inherited role.
In general, this isn’t overly tricky in Laravel. However, as your application scales and you onboard more and more users of different types, suddenly maintaining this within Laravel alone gets tricky. There’s always that one user who wants to have something different to the rest, and the extra work it takes to manage and maintain permissions is hard work.
Following something like RBAC allows you to follow the principle of least privilege, which is a security concept that ensures that users, systems, or processes are granted the minimum access or permissions required to perform their tasks. Nothing more. It helps to minimize access because you are only giving the permissions required to do a specific job. You limit the potential damage from accidental or malicious actions, helping to reduce risk across the board. You can leverage temporary permissions to elevate privileges only when needed, allowing you to revoke them after use.
Authorization in Laravel, the basics
In Laravel we are used to working with Gates, and Policies for managing authorization and permissions, and while this works really well on a small scale - it is very easy to outgrow this model. Let’s look at a really basic example of how we might implement this in Laravel, and add layers on-top until it’s not only usable but scalable.
final class TaskPolicy
{
public function viewAny(User $user): bool
{
return $user->hasVerifiedEmail();
}
}
So, we have a TaskPolicy
that is attached to our Task
model, yes I know a to-do application how original. But, bear with me. We only want to allow users to view any tasks if they have verified their email address. A perfect example of authorization in Laravel, simple, effective, and uses the framework to do what is needed. Image for a second though, that we want to make this feature more competitive in the industry. Our simple flat authorization approach is now going to get more complex because we will have workspaces, teams, projects, etc. Suddenly our authorization logic is going to pile up to a point of brittleness.
Moving to Permissions in Laravel
Let’s move away from basic authorization now, and have a look at how we can handle permissions in Laravel - because there will come a time where you move up from flat authorization and want to implement permissions. Trust me, I’ve done it so many times.
For this example, I am going to assume that you are using the latest version of Laravel, which is Laravel 11 at the time of writing. We no longer have a default AuthServiceProvider
as part of our application, so we need to create a service provider to handle registering the permissions in our application.
php artisan make:provider PermissionServiceProvider
This should automatically register itself for you in the application, but if it doesn’t - just add it to the array in bootstrap/providers.php
. Enough of the worst case scenario though, we are here to learn after all. In our service provider we want to target the boot
method, so that when the application is booted our permissions are created.
final class PermissionServiceProvider extends ServiceProvider
{
public function boot(): void
{
Gate::define('delete-tasks', function (User $user, Task $task): bool {
if (!$user->hasVerifiedEmail()) {
return false;
}
return $user->id === $task->user_id;
});
}
}
We check to make sure that the user has verified their email, and then check direct ownership of the task to the user. We can add additional logical checks here, but you get the picture right?
Then, within our application we can either check the authenticated users ability to do something using the Gate facade, or using the $request->user()->can('delete-tasks')
method, or in our UI using the blade directive of @can('delete-tasks')
. These approaches aren’t terrible, and it is admittedly better than nothing. However, we have these floating values of “permissions” as random strings littered throughout our application that we have no way of managing, no way of controlling easily, and no way of knowing all of the permissions that we actually have without some magic searching. All of which, I am sure you can agree, is quite the headache.
Open source solutions and their limits
We all get to that point in the application’s lifespan where we question if there is a better way to do these things. Typically we will reach for an open-source package first and foremost, it’s low-cost and generally well supported. When talking about open-source packages in the Laravel ecosystem, most of us don’t look much further than Spatie - the company that has packages in at least 80% of all Laravel applications. Their permissions package is extremely well built and tested, and it has stood the test of time in the community. However, each time we want to check permissions it’s a database query. You could look at building a caching mechanism for these, but then you have to manage the caching strategy - cache warming - cache invalidation - etc etc. Not really saving yourself much time or effort there. But also, all of this relies on you, the developer, to make changes and push it through your development lifecycle to get deployed. Maybe you have a super picky person doing code reviews and you have to appease them in the process. All in all, it’s taking hours to get this thing working, deployed, and in place. Then, if any changes are needed, you have to do the whole song and dance again, and likely again.
A more scalable solution: Permit and PDP
So, what is the solution here? What can we do to make sure that we don’t go down this rabbit hole of never-ending support requests. Luckily, the people over at Permit have you covered. You can use their docker image to run what they call a PDP micro-service. PDP is a Policy Decision Point, and is in charge of evaluating your authorization requests. The PDP will ensure zero-latency, great performance, high availability, and improved security.
Once this is up and running, we need to talk with it through our Laravel application. But, currently they do not have a PHP SDK that we can use. Luckily we don’t need a full SDK - just a simple one to talk to the PDP.
docker run -it \\
-p 7766:7000 \\
--env PDP_API_KEY=your-api-token \\
--env PDP_DEBUG=True \\
permitio/pdp-v2:latest
This command will run the PDP locally for you in docker, just make sure that you have your Permit API Token to hand!
Integrating Permit with Laravel
Next up, we want to write a thin service layer around this microservice so that we can make consistent requests to it - without having to write this all by hand every time we need to check roles and permissions.
final readonly class Permit
{
public function __construct(
private string $tenant,
private PendingRequest $request
) {}
public function check(
string $email,
string $action,
string $resource
): array {
try {
$response = $this->request->post(
url: '/allowed',
data: [
'user' => $email,
'action' => $action,
'resource' => [
'type' => $resource,
'tenant' => $this->tenant,
'context' => new stdClass(),
],
]
);
} catch (Throwable $exception) {
throw new PermitApiException(
message: $exception->getMessage(),
code: $exception->getCode(),
previous: $exception
);
}
return $response->json();
}
}
Our Permit class will accept a PendingRequest
into the class, which we will need to configure in our AppServiceProvider - or in a dedicated service provider if that’s more your style.
final class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->app->singleton(
abstract: Permit::class,
concrete: fn() => new Permit(
tenant: config('services.permit.tenant'),
request: Http::baseUrl(
url: config('services.permit.pdp')
)->asJson()->acceptsJson()->withToken(
token: config('services.permit.token')
),
)
);
}
}
Our service provider is going to register our Permit class into our service container, pulling configuration values for the PDP url, API Token, and tenant, then register the instance as a singleton - so we only resolve this one instance in a repeatable way.
As we are using Laravel, we can take this a step further and create a custom Facade, something I like to do for these types of classes.
final class Permit extends Facade
{
public function getFacadeAccessor(): string {
return App\\Http\\Integrations\\Permit::class; }
}
Middleware for Permission Checks
The way in which we can handle permission checks effectively in Laravel, is to use middleware. Middleware in Laravel allows us to transform the request or response, perform any logging and observability, and most importantly - handle permissions and access checks. Here is an example of the middleware we could use to manage permission checks.
final class CheckPermissionMiddleware
{
public function handle(Request $request, Closure $next, string $action, string $resource): Response
{
if (!Permit::check(Auth::user()->email, $action, $resource)) {
throw new UnauthorizedException(
message: "You do not have the permission to [$action] on [$resource]",
code: Response::HTTP_UNAUTHORIZED
);
}
return $next($request);
}
}
Inside our routes, we can implement this middleware quickly and easily. We do, of course, need to register this middleware alias - or use it as a class string implementation that choice is yours.
Route::get('/some-resource', ResourceController::class)->middleware([
'permissions:read,users',]);
This allows us to have a fine-grained and version-controlled approach to managing, maintaining, and implementing our role-based access control. Keeping this in our routes, means that we can block access early on in the request lifecycle. Stopping actions from being performed when they are trying to perform them, which is perfect when it comes to handling forms or managing an API. But, sometimes we have a UI right? How can we enhance the security on the frontend of our Laravel application?
Enhancing Blade Templates
In Laravel though, sometimes we don’t want to block access to an entire route. This is where having a way to only render certain parts of our template depending on a conditional is helpful. However using the Facade or full class in our view isn’t really appealing on the eye, and if you change how it works you have to update code everywhere. So instead, we can create a custom Blade Directive that will act as a shortcut to perform these checks.
Blade::if('permit', function (string $action, string $resource) {
return Permit::check(
email: Auth::user()->email,
action: $action,
resource: $resource
);
});
This will be registered within a service provider, and you can see it a little like a template macro or partial. Anytime you use @permit
this will be used instead in the compiled view, and if you change how this directive works - just rebuild the view cache!
<ul>
@foreach ($users as $user)
<li>
<p>{{ $user->name }}</p>
@permit('delete', 'user')
<a>Delete User</a>
@endpermit
</li>
@endforeach
</ul>
We can use the rest of our Blade template in the exact same way as before - just enhancing the developer experience by leveraging our new Blade Directive to interact with Permit PDP where we need it. This will not render if the user does not have the permissions to delete a user for example.
In Conclusion
Let’s recap a moment. We went from the basics of Gates and Policies, which allowed us to have in place permission checks and ownership checks. This is great when you get started, but in reality, you are going to be creating work for your future self to maintain and manage this in the long run. Then we moved onto using third party open-source packages which will implement RBAC for us, which is pretty reliable and pretty popular. The biggest downside here is that it starts to become a management headache over time. Finally, we reached Permit and the PDP. The holy grail solution. A central dashboard to manage and control the access policies and a local relay client that will cache policies to reduce latency for permission checks.
Using a solution like Permit not only helps you to manage the roles and permissions effectively, but allows you to scale out your roles and permissions horizontally if you should need to split into macro or micro services later down the line too.
As you can see, it isn’t overly difficult to go beyond just the basics of what Laravel offers - it just takes an understanding of the Laravel ecosystem, and how it works. So, the next time you are thinking of adding roles and permissions to a Laravel application - permit yourself to try something else, like Permit.
Written by
Steve McDougall
Experienced CTO with scaling products and Dev teams, a massive advocate for the PHP language, community organizer, open source enthusiast, and conference organizer.