Laravel authorization best practices and tips
When using roles and permissions, only authorize based on permissions
With the Laravel Permission package by Spatie, you get access to roles and permissions. They provide the ability to assign each to a user and later check for their existence. You can check to see if a user has a role or has permission assigned to them. In addition, permissions can be applied to roles, and users inherit those permissions.
This flexibility is great, but mixing checks for roles and permissions, and assigning them haphazardly, can cause maintenance nightmares. If you’re not always doing authorization in the same way and following a secure paradigm, you could even introduce unwanted misconfigurations and holes in your application that hackers could exploit.
Permissions can and should be assigned to roles, not individual users. Then, you should only check for permissions, never for roles. Users can be assigned many roles, and therefore they will inherit a collection of permissions. This ensures a predictable yet flexible way of providing authorization. If authorization needs to change in the future, you only need to change which permissions apply to which roles. You don’t have to edit individual user’s permissions or change their roles.
Time for an example. We have installed Laravel Nova, and we want to authorize access to Nova. This is done in the Nova Service Provider with a callback. Imagine you only want admin users to access it. You might think to do this:
use Laravel\Nova\Nova;
Nova::auth(function ($request) {
return $request->user()->hasRole('admin');
});
This is not the best solution, though. Instead, you should create permission that just reflects access to Laravel Nova. Then, check for that specific permission. (After we get into Nova, other permissions and policies may be applied. This permission and check simply authorize access to that module in general.)
So, let’s do the following:
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
$accessNova = Permission::create(['name' => 'access-nova']);
$adminRole = Role::whereName('admin')->firstOrFail();
$adminRole->givePermissionTo($accessNova);
And now, we update our Nova::auth() callback:
use Laravel\Nova\Nova;
Nova::auth(function ($request) {
return $request->user()->hasPermissionTo('access-nova');
});
Now, instead of checking for a role, we’re checking an overall permission. This is more verbose and gives us the flexibility we need. Now, for example, if we ever wanted the support role to access Nova, we could run the following code:
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
$supportRole = Role::whereName('support')->firstOrFail();
$supportRole->givePermissionTo('access-nova');
This is just one example. Another area that you might run into this is gate and policy interception methods (the before() method). I recommend checking against permissions there as well. Create permission like update-any-post instead of checking for the admin role.
Streamlining policy code
When writing Policies, the code can get kind of repetitive. Two places that usually are duplicated are interception methods and update/delete methods. But, there are easy ways to simplify and reduce duplicate code.
First, let’s talk about the interception methods. In my Policies, I tend to have a before() method that checks for a higher-privileged user’s ability to bypass the individual checks. I always write it the same: check if the user has a role named admin and return true if they do. Then, I’d create an abstract class called BasePolicy which contained this one method. All of my policies extended that.
But now that you’ve read my earlier best practices, you can see the flaw in that logic. We shouldn’t be checking for a role. But, we can still use a dynamic method and a trait in our policies. Let’s assume we want most of our policies to check an interception method. We could write a trait like this:
namespace App\Policies;
use App\Models\User;
trait HasInterceptionProperty
{
protected method before(User $user)
{
if ($user->hasPermissionTo($this->interceptionPermission)) { return true;
}
}
}
Then, a policy can apply this trait by defining the variable interceptionPermission and using the trait definition like so:
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
use HasInterceptionProperty;
protected $interceptionPermission = 'crud-any-post';
public function update(User $user, Post $post)
{
return $post->user_id === $user->id;
}
}
Now, this PostPolicy will check the interception method with the proper permission. If the user has crud-any-post it will immediately allow the action. Otherwise, an update request will check for the Post.user_id field to match the current user’s id field.
Now, I can admit that this doesn’t seem like a lot of saved work in this example. But, as your interception logic grows, then you can start to see the benefit. Also, I’d rather define a unique property per class than rewrite the same logic repeatedly.
Rewriting the same logic brings us to our second way to streamline the policy code. Often, you’ll find that the update() and delete() methods have the same business requirements. So, why not make a protected method and have them return the results of that shared method?
In practice, though, I tend to write my logic in the update() method and have the
delete() method as a proxy. This is perfectly fine. It might look like this:
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
public function update(User $user, Post $post)
{
return $post->user_id === $user->id;
}
public function delete(User $user, Post $post)
{
return $this->update($user, $post);
}
}
Both updating and deleting require the same logic. There’s no need to write it twice.
Move complex gate functionality to invokable classes
The documentation for Gates in Laravel shows two ways to define them: inline closures or as a callable method. I always found the example of the callable method confusing, though. If the gate is only going to call a method on a policy, what’s the point?
There’s a really good use case for this functionality. Let’s imagine a scenario where we have a really complex Gate based on several business rules. Separating this into a separate class can allow us to keep our AuthServiceProvider smaller and cleaner. It also allows us to have a class to easily write unit tests against, perhaps even without having to boot up the entire Laravel framework.
Let’s imagine we have a User model that we want to gate allowing trades on our platform. The user has points that maybe measure how active they’ve been. They must have at least 100 points. They also have a multi-tiered subscription possibly. Finally, they may have already made trades.
The total amount of allowed trades is based on their subscription. Tier 1 allows up to 100, Tier 2 up to 200, and Tier 3 allows unlimited.
As you can tell, this is already getting complex. And this is only the beginning. The business has plans to expand the complexity of this gate.
Let’s look at it in the form of a gate class.
namespace App\Gates;
use App\Models\User;
class AllowedToTrade
{
public function __invoke(User $user)
{
$hasEnoughPoints = $user->points > 100;
$level = optional($user->subscription)->level;
return $hasEnoughPoints
&&
(
($level === 1 && $user->trades_count < 100)
||
($level === 2 && $user->trades_count < 200)
||
$level === 3
);
}
}
As you can tell, this would end up getting pretty long and unwieldy as an inline closure. Now, we can simply define our gate like this:
use App\Gates\AllowedToTrade;
use Illuminate\Support\Facades\Gate;
Gate::define('allowed-to-trade', AllowedToTrade::class);
Nice and simple.
Authorizing unauthenticated users in Laravel
I claimed that authorization and authentication go hand-in-hand in a previous article, but one doesn’t necessarily require the other.
Specifically, we can authorize actors that we haven’t identified with authentication. Great, but how do I do that in something like a Gate or a Policy?
The Laravel documentation reminds us that all Gates and Policies will return false for any unauthenticated user. That is unless we type hint to the user as optional.
So, let’s imagine a scenario where a gate requires a user to have specific permission to pass the check. However, any visitor in the home office with the static IP of 240.0.0.1 is allowed.
We might write the gate like this:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Request;
Gate::define('allow-super-secret-access', function (?Models\User $user) { return optional($user)->hasPermissionTo('do-the-thing') || Request::ip() === '240.0.0.1';
});
Now, we can see if the user is defined and has permission to do the thing. They’re allowed through the check. Otherwise, if they don’t have that permission or don’t even know who they are, the request IP address is compared to our home office IP.
Checking for existence of relationship data
Laravel provides functionality for relationship methods on Eloquent models in two different ways. First, calling the method directly returns a Query Builder for that relationship. The other way to access the relationship method is to refer to a property named after the method. This loads the data from the Query Builder into a collection into that property on the model.
Depending on what has happened to your model during the request lifecycle of the application, it may have hydrated data on its relationship property, or it may not. It’s inefficient to load in a collection of data when you only want to check if at least one record exists. However, it can also be inefficient to run another query against the database to check for existence if you already have a populated collection. What makes this more confusing is accessing the property in most ways causes it to proactively load the collection, even if that wasn’t your intent.
I know this might be confusing. But, there’s an easier way. Perhaps this example code can clear it up.
Let’s imagine a policy that only allows us to create a post if we have at least one subscription available. We’re not sure if our User model had loaded the subscriptions property earlier in the request lifecycle, and our policy doesn’t need to care. Our policy looks like this:
namespace App\Policies;
use App\Models\User;
class PostPolicy
{
public function create(User $user)
{
if ($user->relationLoaded('subscriptions')) {
return $user->subscriptions->isNotEmpty();
} else {
return $user->subscriptions()->exists();
}
}
}
When the create() method is run, it checks for both scenarios and uses the most efficient method. If the user already has subscriptions loaded, it can use the Collection::isNotEmpty() method to verify the collection contains at least one subscription. Otherwise, there’s no reason to retrieve all the data. The other side of the if statement does a quick query to determine if the count of rows for this user in the subscription table is greater than 0. This way is much more efficient, with no chance of accidentally loading data you didn’t intend to.
TLDR; what’re your Laravel tips?
Only authorize against permissions, not roles. Users can have many roles and will inherit all of the associated permissions. Policies can be streamlined by using traits and proxies. Gates can be defined as invokable classes. Type-hinting the User model as optional allows authorization in gates and policies of unauthenticated users. Eloquent provides a method called relationLoaded() to check if you should query a relationship collection or do a database call when checking for existence.