Laravel Security: Understanding Policies & Gates

Laravel 15 January 2024 2 minute read

What's a policy? What's a gate? It can be difficult understanding the difference between the two. Let's go over which does what and how to apply them to your Laravel app.

Note: The following tutorial assumes we're working on a blog app. The database has two tables: users and posts.

Policies vs Gates

Although the terms are sometimes used interchangeably, policies and gates are different. That being said, you don't tend to create one without the other.

A policy is the rule you're trying to implement. Think something like "A logged in user can access this page, but a guest cannot".

You then use a gate inside a controller to implement the policy.

The policy is the rule (like a clearance level) and the gate is the check (like a security gaurd).

Creating Policies for Models

As Laravel is very CRUD-based (Create Read Update Delete), you'll usually create a policy to tell Laravel what the current user can do with with a given model (the model being a database record).

You can use Artisan to create a policy for a model like so:

php artisan make:policy PostPolicy

Important: Laravel uses some interal "magic" to know a model's policy rules without you having to explicitly do anything. The naming convention here is very important since Laravel will automatially assume the policy for a Post model is called PostPolicy and it's inside the app/Policies directory.

The policy will look like this:

<?php

namespace App\Policies;

use App\Models\User;

class PostPolicy
{
    /**
     * Create a new policy instance.
     */
    public function __construct()
    {
        //
    }
}

You can now implement any method you like to create a rule, however it's a good idea to stick to the classic RESTful method names. This will correspond to controller method names to make things easier to piece together later on.

Here's an example of an update rule for the Post model inside the PostPolicy:

<?php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    public function update(User $user, Post $post)
    {
        return $user->id === $post->user_id;
    }
}

Important: As we've typehinted a User in the update method, Laravel will automatically fail the check if the current user is not logged in.

As you might be able to see, creating a rule for a policy is usually a one-liner to check some IDs match. That's not always the case, especially when your app gets more complex, but for the most part that's all you need to do.

The above code checks to see if the current User is the same as the user_id property on the Post record.

All you need to do in a policy is return either true or false. True means the rule passes whereas false means it fails.

Implementing Policies with Gates

Controller Methods

The easiest way to implment a policy is by using a gate. A gate is really just an if statement to see if the policy rule passes or not. If it returns true you can continue on with your controller logic. If not you can do whatever you want, like redirect the user to the homepage.

Here's how you can use the Gate facade in a controller method to automatically send the user a 403 forbidden error if they fail the authorisation:

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;

class PostController extends Controller
{
    public function update(Request $request, Post $post)
    {
        Gate::authorize('update', $post);

        $post->update($request->only('title'));

        return \redirect()->route('posts.index');
    }
}

Laravel will automatically stop code execution if the Gate::authorize() method fails and handle the exception for us.

The Gate facade has a load of helpful methods on it. Here are a few to note:

  • Gate::authorize(): Throw an exception if the check fails.
  • Gate::allows(): Return true or false.
  • Gate::inspect(): More granular control over the state of the check. You can assign it to a variable called $check and do things like $check->allowed() and $check->denied().

Form Requests

If you're not familiar with form requests this part might be a little hard to follow, so I suggest you read up on Laravel Form Requests first.

In our example blog app, we have a form request for updating a post like so:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class PostUpdateRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:255'],
        ];
    }
}

To implement the policy here, we can use the Gate::allows() method:

Gate::allows('update', $this->route('post'));

Here's what that looks like in the form request:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;

class PostUpdateRequest extends FormRequest
{
    public function authorize(): bool
    {
        return Gate::allows('update', $this->route('post'));
    }

    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:255'],
        ];
    }
}

Then in our controller we need to typehint our custom form request instead of using the default request:

<?php

namespace App\Http\Controllers;

use App\Http\Requests\PostUpdateRequest;
use App\Models\Post;

class PostController extends Controller
{
    public function update(PostUpdateRequest $request, Post $post)
    {
        $post->update($request->only('title'));

        return \redirect()->route('posts.index');
    }
}

The nice thing about this (also my preferred method for implementing policies) is that you remove all the authorisation stuff from the update method.

This means the method only has code for updating the post and redirecting the user, which makes it feel much cleaner.

It's also much easier to process visually what it does when you come across it.

In Conclusion

Laravel gates and poilicies are an incredibly powerful feature to give your app some serious security. A good example is a blogging system. You may never want a user to be able to update a post written by a different user. Gates and policies are a perfect solution for that.

Keep in mind it can be painful (and a little boring) to go back through an app to add policies after the fact, so make sure you create and implement them as you go.