How to use authorization in Laravel: Gates, policies, roles and permissions
In a previous entry, I mentioned the importance of hand-in-hand authorization and authentication. Now, let’s talk about the many ways that Laravel provides to apply authorization to your application.
The Laravel documentation describes multiple tools to authorize access to your application. It goes into detail about creating, constructing, and applying these authorization mechanisms. However, it only gives light direction about which method is best to use in your application. That’s because each application is different, and the way you apply authorization can be subjective. One of the packages I describe later, Spatie’s Laravel Permission, also walks the same tightrope. They make sure to integrate with Laravel and provide robust features but generally hint at guidance.
So, how do you decide what authentication mechanism to apply? Do you use Laravel’s built-in tools, or must you install a third-party package to get the functionality you need?
This question is complicated, but we can work towards an answer. Let’s begin by examining what we have available to us.
Authorization tools available in a Laravel app
Laravel provides gates and policies right out of the box. You can read the authorization documentation for detailed implementation instructions. But let’s talk specifically about each and what they’re best used for.
Gates are more high-level and generic. While they can be applied to specific objects (or Eloquent models), they tend to be more business process-oriented or abstract. I like to think of this by picturing a gatekeeper. You have to get past the gatekeeper or bouncer to get into the club. Inside the club, that’s a whole different story as you interact with individuals. That’s more along the lines of policies.
Policies tend to match up nicely with the basic CRUD (Create, Read, Update, Delete) mechanics. By design, they provide a paradigm for these actions.
Policies tend to be applied to a resource, like an Eloquent model, to determine if the user can do one of the CRUD actions. They’re not limited to these actions, though. They can be expanded with custom actions and used to authorize relationships. (Laravel Nova uses registered policies to determine if you can do things like creating or attaching models to a relationship.)
Gates and policies offer a mix of fine-grain and abstract authorization, but they lack a hierarchy or grouping functionality. That’s where the Laravel-permission package by Spatie shines. It provides a layer that both Gates and Policies can take advantage of. This functionality includes roles, permissions and teams.
Permissions describe if a user can do an action in a general, non-specific sense. Roles describe the role a user may play in your application. Users can have multiple roles. Permissions are applied to roles. (Permissions can be applied to a user directly, but I’d advise against that.) Finally, and optionally, teams are groups of users. Those Teams can contain many roles.
Now you know what’s all available. See how this could get confusing? There seem to be many ways to solve the same problem if you’re looking at these definitions. Hopefully, things will clear up when we look at these in practice.
Laravel gate example
In this scenario, I want to ensure my user has gained at least 100 points to see the link for redemption. I’m storing the number of points directly on the user
model.
Let’s see what the gate might look like:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::define('access-redemptions', function (User $user) {
return $user->points >= 100;
});
Now, let’s see what our navigation HTML in the Blade file may look like:
<nav>
<a href="{{ route('dashboard') }}">Dashboard</a>
@can('access-redemptions')
<a href="{{ route('redemptions') }}">Redemptions</a>
@endcan
</nav>
Here, we can see that we’re using Laravel’s Blade @can
directive to check the authorization of this action for the current user.
To complete the full check, we’d probably add something like this at the top of our method accessed for the redemptions
route:
use Illuminate\Support\Facades\Gate;
Gate::authorize('access-redemptions');
If the user did not have permission to access-redemptions
, an authorization exception would be thrown.
So, let’s break this down so we can tell why this was the perfect use for a gate:
- It is a generic action: “can we access a business process” is basically what the question is. Can I access redemptions? Well, only if you have 100 or more points.
- Even though it depends on the current user, an Eloquent model, it doesn’t apply to another model. We’re not checking some external resource or model for points. Instead, we’re looking at ourselves, what we know about our state, so that means it’s probably not something a policy would work with.
- We need to do a calculation, so roles and permissions are out.
They only provide a binary determination if an action is allowed or not.
Get your free course catalog
Laravel policy example
To demonstrate a policy, let’s pick a simple example. I want to authorize only owners of a book to update it. Each book has a user_id
field that represents the owner.
Here’s what that policy class would look like:
namespace App\Policies;
use App\Models\Book;
use App\Models\User;
class BookPolicy
{
public function update(User $user, Book $book): bool
{
return $book->user_id === $user->id;
}
}
Now, we want to authorize our controller method. Normally I’d recommend using a resourceful controller with the authorizeResource() helper. But, let’s demonstrate this in a more verbose way by applying it directly in the update()
method of a BookController
.
namespace App\Http\Controllers;
class BookController extends Controller
{
public function update(Book $book, Request $request)
{
$this->authorize('update', $book);
// ... code to apply updates
}
}
The BookController::authorize()
method, or authorization helper, will pass the current user into the BookPolicy::update()
method along with the updated instance of the $book
. If the policy method returns false
, an authorization exception would be thrown.
Why is a Policy the chosen authorization tool? First, we are working with a specific type of action: we have a noun and want to do something. We have a book, and we want to update it in this case. Second, since it’s a specific Eloquent model, a Policy is the best tool to work with individual items. Finally, because this is a CRUD type action, and we’re already following the paradigm of naming methods after their action in the controller, that’s a great hint that we should be using the same method names in a policy named after that model.
Laravel role and permission example
To demonstrate the Role and Permission authorization tools, let’s think about an organization with departments. In that company, there is a sales team and a support team. The sales team can see client billing information but cannot change it. The Support team can see and update client billing information.
In order to accomplish this, I want two permissions and two roles. Let’s set them up:
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
$sales = Role::create(['name' => 'sales']);
$support = Role::create(['name' => 'support']);
$seeClientBilling = Permission::create(['name' => 'see-client-billing']); $updateClientBilling = Permission::create(['name' => 'update-client-billing']);
$sales->givePermissionTo($seeClientBilling);
$support->givePermissionTo($seeClientBilling);
$support->givePermissionTo($updateClientBilling);
We’ve registered two roles and applied the appropriate permissions to each role. Now, users who have these roles will inherit those permissions as well.
Now, let’s see a few methods in our billing controller.
namespace App\Http\Controllers;
use App\Models\Client;
use Illuminate\Http\Request;
class ClientBillingController extends Controller
{
public function show(Client $client, Request $request)
{
abort_unless($request->user()->can('see-client-billing'), 403);
return view('client.billing', ['client' => $client]);
}
public function update(Client $client, Request $request)
{
abort_unless($request->user()->can('update-client-billing'), 403);
// code to update billing information
}
}
Now, if a user visits ClientBillingController::show()
with either role of sales
or support
they will have access to see the billing information. Only users with the role support
, which gives them permission to update-client-billing
, will submit to the update()
method.
Why are Roles and Permissions the right authorization choice? You could accomplish the same sort of thing with Gates or, to some extent, policies. But, roles and permissions make it easier to understand and apply the permission approach in only one location. Let’s say in the future you want Sales to be able to update Client billing information as well: you’d only have to add the update-client-billing
permission to the sales
role. One quick change. You wouldn’t have to check various gates or track down policies. This type of action, which is not necessarily unique to a specific model but provides levels of access or authorization, makes roles and permissions the perfect tool.
TLDR; which authorization mechanism should I use?
Gates are used for specific functionality outside the standard CRUD mechanisms. And, they’re great for vast and sweeping access to whole sections or modules. Policies work best with the CRUD paradigm, authorizing specific objects or eloquent models. Roles and policies work well when each group or department needs to accomplish specific actions. Functionally, there is a lot of overlap for each tool, so you might find yourself mixing and matching. Also, you may integrate one inside another (like permission checking combined with ownership verification in a policy).