Securing API Endpoints With Laravel Gates & Policies

A sample chapter from Building APIs & Single-Page Applications with Laravel + Vue.JS + Capacitor

In the last section, we went through adding an admin middleware to block off certain API endpoints. While this works, I prefer to use gates and policies wherever we can. It allows us fine grained control in complex scenarios. If you view the ROAST API source code, you will see that we have a variety of policies implemented and applied as middleware. Let’s go through this process.

Step 0: Why Policies instead of Gates?

So why polices instead of gates? For me, I like to keep all permissions in a single file. It allows for scalability in the future, clean code encapsulation, and I know where to look if I need to make changes. Kind of like a bucket once again. We will have a bucket to place our permissions in and as we expand functionality. Policies are excellent for features that are accessed from multiple permission levels since you can account for whether a user in a variety of different scenarios can access a resource.

Let’s add our first policy!

Step 1: Create A Company Policy

We can do this with a simple artisan command. Our first policy will be for our Company resource so let’s run:

php artisan make:policy CompanyPolicy

Since we haven’t created any policies yet, this will create an app\Policies directory and add a CompanyPolicy.php file within the directory. We will come back to this as soon as we register our policy.

Registering our policy will let Laravel know it exists and can be used on requests. There are ways to autoload these polices or “discover” them. I prefer to be verbose and register them. To do that, open up the app/Providers/AuthServiceProvider.php file and add the following to your use declarations:

use App\Models\Company;
use App\Policies\CompanyPolicy;

Now that we have the classes being used in the auth service provider, we can map the class to the policy like this:

/**
 * The policy mappings for the application.
 *
 * @var array
 */
protected $policies = [
    Company::class => CompanyPolicy::class
];

What this will do is automatically bind the model we are validating into the policy so we can access it as needed. This will be very important for our owner permission level since we will need to see if the user is a registered owner on the company or other entity. With all of our policies, we will be registering them here. For example, when we add our CafePolicy.php we will be adding the following lines of code:

use App\Models\Cafe;
use App\Policies\CafePolicy;

and

/**
 * The policy mappings for the application.
 *
 * @var array
 */
protected $policies = [
    Company::class => CompanyPolicy::class,
    Cafe::class => CafePolicy::class
];

When looking at the company policy, the routes that should be protected by this policy are the routes used to create a company, edit a company, or delete a company. All three of these methods have the same logic. They can be processed if the user is an admin or an owner of the company.

However, if the user is not valid, we want to allow the user to submit a temporary action. Because of this, we won’t be applying this policy as a middleware, but a little deeper and in the services themselves. The whole “temporary action” feature is kind of app specific, but also an example I’ve used in a lot of different apps. Check out the next chapter for more on the implementation.

Step 2: Add Policy Logic

When looking at a basic resource, such as a company, within our application we need to provide logic in our policy to handle the basic modifications for an entity. This will give permission on whether the user can modify resources or create resources based off of their user permission.

Let’s go back into our app\Policies\CompanyPolicy.php and stub out the following methods:

public function store( User $user )
{

}

public function update( User $user, Company $company )
{

}

public function delete( User $user, Company $company )
{

}

also make sure we add the proper use declarations on top of the file:

use App\Models\User;
use App\Models\Company;

In this instance, we only want the modification of the resource to have certain authorization parameters. We don’t need any authorization on viewing any piece of this resource. Later on, with other resources such as job applications or event sign ups, we definitely will need an authorization based protocol set up to handle this since we don’t want to leak public data.

Let’s take a look at the store() and update() methods, the delete() method will follow the same logic as the update() method.

The store() method signature accepts an injected instance of the user which will be the currently authenticated user. To store a new company directly in the database, we will need to be an admin. Since it’s a new company, there is no direct owner yet. That would be assigned after the creation. The final implementation of this method will look like:

public function store( User $user )
{
    if( $user->permission == 'admin' ){
        return true;
    }else{
        return false;
    }
}

This method simply accepts a user, which will be the authenticated user. If the user has a permission level of admin we return true meaning they have permission to perform the proper storage. If they are not an admin, we return false blocking them from performing the action. We will handle both of these scenarios in the next step.

With an API Driven Application, we want the route to be accessible to users but we need to apply this permission on the API side since any SPA “security” should be deemed unworthy. Remember the entire SPA is loaded client side. If someone wanted to really modify the code, they could and that could wreak havoc on our database.

Next, let’s check out the update() method. The update() method has an additional parameter passed which is an instance of a company model. Since we are updating an existing company we can apply some different logic and a little more fine grain control. Remember our owner permission? A user who is not an admin can own a company and therefore manage that specific resource. Let’s take a look at what that looks like:

public function update( User $user, Company $company )
{
    if( $user->permission == 'admin' ){
        return true;
    }else if( $user->companies->contains( $company->id ) ){
        return true;
    }else{
        return false;
    }
}

When we call this method in the next steps, we will be passing the company along with the user. The first layer of security is if the user is an admin. If they are, then return true they can definitely perform the action. Next, we check if the user’s company relationship contains the id of the company.

Remember, this is the many-to-many relationship that we set up. We can call the contains() method on the relationship and pass in the id of the entity we are checking to see if there is a relationship set up. If there is, that means the user is an owner on the company and we return true. Finally, if neither of those scenarios are true, we return false and the user doesn’t have any of the necessary permissions. The delete() method is implemented the exact same way with only allowing admins and owners to perform the method.

Now that we have our policy logic laid out, we can begin to implement the logic within our application. As our app grows and we add more policies, this flow for resources will generally remain same. Even our cafe policy follows a similar structure since it is a nested resource. If the user has permission to manage the company, they have permission to manage the cafe as well. When we add more features, if we ever come across a more unique scenario, I’ll for sure document it and explain it in full. It’s time to begin the implementation.

Step 3: Implementing Policies within Code Blocks

One of the ways we can implement a policy into our application is through blocks of code. Policies have specialized method named can() that literally checks if the authenticated user can perform the action. You will see a mixture of code block implementation and middleware implementation in ROAST. We are doing this as a code block now because we want un-authorized users to submit data, we will just handle it differently.

Let’s start with the most basic example of creating a resource, in our case a company. To do this, look at the app\Services\Company\CreateCompany.php service and find the save() method. In there, let’s do a little refactoring to implement our policy.

First, let’s extract the code from the existing save() method and make a new private method named persistCompany():

private function persistCompany()
{
    $company = new Company();

    $company->name = $this->data['name'];
    $company->roaster = $this->data['roaster'];
    $company->subscription = $this->data['subscription'];
    $company->description = $this->data['description'];
    $company->website = $this->data['website'];
    $company->address = $this->data['address'];
    $company->city = $this->data['city'];
    $company->state = $this->data['state'];
    $company->zip = $this->data['zip'];
    $company->facebook_url = $this->data['facebook_url'];
    $company->twitter_url = $this->data['twitter_url'];
    $company->instagram_url = $this->data['instagram_url'];
    $company->added_by = Auth::user()->id;

    $company->save();

    $this->saveImages( $company, $this->data );

    return $company;
}

Now that we have that method in place, let’s go back to the save() method and implement the policy logic:

public function save()
{
    if( Auth::user()->can('store', [ Company::class ] ) ){
        return $this->persistCompany();
    }
}

This is all we need to do to ensure we have a proper user who can perform a persistence to the database! Like I’ve mentioned before, to perform an action on an authenticated user who doesn’t have permission, check out the source code for how we set up our temporary data structure.

Let’s digest this a little bit. We got to this point through a couple different forms of security. When a user sends a POST request to /api/v1/companies we first check if they are authenticated through the middleware. If they are authenticated, we validate that the data they are sending is what we need through a request. Once that is validated, then we hit this method to determine if they can actually perform this function.

That may sound a little backwards for some scenarios and it is. If we were entirely blocking off the method before even checking the data, we would be applying the policy as middleware instead of after the validation request. The reason we do this after the request, is because later down the logic stream, we determine how we want to perform the action. Do we want to persist the data right away if they are an admin? Or, do we want to create a temporary action and handle the request manually at a later point?

Now with this method, let’s take a look at the first line:

if( Auth::user()->can('store', [ Company::class ] ) )

What this does is check to see if the currently authenticated user can run the store method. The second parameter is where it gets a little bit different. Since we aren’t checking permission on a specific model, we just pass the class for the policy we want to use. Remember when we registered the polices in the AuthServiceProvider.php? This is why we did that. We could have multiple store permissions (and we do when storing a cafe), so we need to direct Laravel to check the store permission on the Company policy. That’s why we pass the Company::class.

If that passes, meaning the user is an admin, we run the persistCompany() method that saves the company right away!

When I first implemented these authorization methods, I was like why don’t we just put it all in the if statement in the code? I mean, that’s what we are doing anyway? Well, when I had to add permissions and check for different parameters later on, we had one single place we could update to do that, and that was in the policy. So when I used the permissions in like 10 different areas, I only had to update it once which was awesome! Policies completely organize code and have a single point of entry.

To take a look at another scenario, open up the UpdateCompany.php and look at the update() method. We once again, refactored the code out if their into a persistUpdates() method and applied the appropriate policy:

if( Auth::user()->can('update', $this->company ) ){
    return $this->persistUpdates();
}

So the difference between the create and update policy method is that in the update method accepts an actual instance of an resource that we are checking the policy against. When updating a company, we have the company we are updating, injected into our route so we know which specific company it is. The route is blocked by middleware and the data is already validated, we just determine what we want to use to handle the request, should we persist it, or handle the action later?

So that’s one way of applying policies, the next is to apply it through middleware.

Step 4: Implementing a Policy as Middleware

In order to give an example on how to implement a policy through middleware, we are going to create a basic resource that only admins can manage. This way, it’s a cut and dried way of blocking off a large chunk of functionality that no other user “might” be able to edit, like an owner of a resource.

What we will be doing is management of Brew Methods. Brew methods in our application are ways that a cafe can brew coffee such as espresso, pour over, siphon, etc. We want these Brew Methods to be managed by the admin if a new method becomes available or an industry name changes, etc. We will be able to filter cafes by what brew methods they offer. Those who enjoy their coffee a certain way will definitely find this valuable!

The concept of an admin only resource can be used in a variety of scenarios such as managing teams, updating permissions, or really anything else that relates to the business logic of an app.

For this example, I went ahead and actually built the entire functionality for our brew method management. I’ll share what is necessary to make sense for creating a policy.

So the Brew Methods resource has the standard routes for any resource, an index, show, store, update, destroy. Unlike other resources, we have some unique permissions. We want the index route to be publicly available, but every other route to be only accessible by an admin. No other user should access these routes, not even owners. Because of this black and white difference, we can apply a middleware to these routes.

Let’s open up the app/Http/Controllers/API/BrewMethodsController.php and look at the __construct() method:

public function __construct()
{
    $this->middleware('auth:sanctum’)->only('show', 'store', 'update', 'destroy');
    $this->middleware('can:show,method')->only('show');
    $this->middleware('can:store,App\Models\BrewMethod')->only('store');
    $this->middleware('can:update,method')->only('update');
    $this->middleware('can:delete,method')->only('delete');
}

This is where we apply all of the middleware on a controller. Like I mentioned before, I like doing this because it keeps our routes file clean and focuses the permission and auth logic in a convenient location.
Let’s take a look at the middleware:

$this->middleware('can:show,method')->only('show');

What this is doing, is blocking off the show() method by utilizing our policy. The middleware checks if the authenticated user can perform the show action. The syntax for this is a little weird because after the ,, we have method. Now remember when we set up our dependency injections? Well on our Brew Method routes, it’s binding to the method route parameter and injects the brew method into that route.

With the show command, we are ensuring that the authenticated user is authorized to show a specific brew method. This is the same for the update and delete methods as well. The only difference is the store method which accepts the App\Models\BrewMethod as the class itself. This is similar to when we applied the policy to our other routes that didn’t accept a specific entity.

We’ve also applied the polices as middleware to their corresponding routes by using the only() method. Now there’s another way you can apply policies and that’s through controller helpers. This would involve adding a line similar to: $this->authorize('update', $brewMethod) in the actual controller function itself.

Why didn’t I do it that way? I like keeping those controller methods as clean as possible. The less business logic in the method itself, the better. I also feel that since we can apply this on the middleware level, we can stop even reaching that method if we don’t have to and we can keep all security of the controller in one convenient location. This brings us to the next subject.

The order of middleware counts! If you look back at the __construct() method, we still have our authentication middleware as the first one: $this->middleware('auth:sanctum’)->only('show', 'store', 'update', 'destroy'); . Why do we have that first? Think of it as the biggest gate. Is the user authenticated or not? If they aren’t authenticated, then we know for sure they are not an admin so we block the access right away. We also return a 401 error which means Unauthenticated compared to a 403 Unauthorized error if the middleware fails.

There will be a lot more examples of using policies throughout our application. Anytime anything unique comes up, I’ll document it and explain it. If you want to see the full source code, check out Gitlab and see it live in action (Note: Gitlab access is available in our “Complete Package“).

Now that we have our polices implemented, let’s take a look at request validators, then move to the NuxtJS side of the application and create the corresponding middleware.

Get the book and more
Choose the package right for you.
The Essentials
$45 $35 USD
The Ultimate Guide to Building APIs & Single Page Applications with Laravel + VueJS + Capacitor Book Cover Vue 3/Nuxt 3, Laravel 10, Capacitor 3 The Ultimate Guide to Building APIs & Single Page Applications with Laravel + VueJS + Capacitor Book Cover Vue 2/Nuxt 2, Laravel 8, Capacitor 2
Green check mark for features included with purchase BOTH Editions of the 500+ page book (for Nuxt 2 and Nuxt 3)
Green check mark for features included with purchase Sketch & Figma icon templates optimized for Capacitor
Green check mark for features included with purchase Lifetime access & updates
Buy Now
The Complete Package
$295 $145 USD
Icon for videos included with book The Ultimate Guide to Building APIs & Single Page Applications with Laravel + VueJS + Capacitor Book Cover Vue 3/Nuxt 3, Laravel 10, Capacitor 3 The Ultimate Guide to Building APIs & Single Page Applications with Laravel + VueJS + Capacitor Book Cover Vue 2/Nuxt 2, Laravel 8, Capacitor 2 Icon for source control and community access included with the book
Green check mark that includes the book with purchase BOTH Editions of the 500+ page book (for Nuxt 2 and Nuxt 3)
Green check mark that includes the source code with purchase Production source code access to an app called ROAST
Green check mark that includes private forum access with purchase Private forum access where you can meet & get help from others
Green check mark that includes in depth video tutorials with purchase In-depth video tutorials
Green check mark that includes Sketch & Figma icon templates optimized for Capacitor with purchase Sketch & Figma icon templates optimized for Capacitor
Green check mark that includes lifetime access with purchase Lifetime access & updates
Buy Now
❤️ Github Sponsors get an additional 20% off the book! Learn more →