Skip to content
Muhammet Şafak
tr
Web Development 4 min read

Authorization with Laravel Gates and Policies

How to centralize authorization logic in Laravel using Gates and Policies, keeping 'who can do what' decisions clean and testable.


Authorization — the question “can this user perform this action?” — has to be answered somewhere in every application. The question is simple, but where you answer it matters. Writing checks like if ($user->id !== $post->user_id) inside controllers looks harmless at first; but once the same check is scattered across several places, changing a single rule means hunting down dozens of spots.

Laravel’s Gate and Policy system makes these decisions centralized and testable.

What is a Gate?

A Gate is used for simple, non-model-based authorization checks. It is defined inside AuthServiceProvider:

use Illuminate\Support\Facades\Gate;

Gate::define('update-settings', function ($user) {
    return $user->is_admin;
});

Using it:

if (Gate::allows('update-settings')) {
    // Admin action
}

// Or shorthand:
Gate::authorize('update-settings'); // Throws 403 if not allowed

It also works in Blade templates:

@can('update-settings')
    <a href="/settings">Settings</a>
@endcan

What is a Policy?

A Policy is a class that groups the authorization logic specific to an Eloquent model. For models with multiple actions, it provides a cleaner structure than a Gate.

To generate one:

php artisan make:policy PostPolicy --model=Post

The generated PostPolicy class defines authorization rules as methods:

namespace App\Policies;

use App\User;
use App\Post;

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

    public function delete(User $user, Post $post): bool
    {
        return $user->id === $post->user_id || $user->is_admin;
    }
}

It needs to be registered in AuthServiceProvider:

protected $policies = [
    Post::class => PostPolicy::class,
];

Using a Policy

Inside a controller:

public function update(Request $request, Post $post)
{
    $this->authorize('update', $post);

    // Execution only reaches here if authorized
    $post->update($request->validated());
    return redirect()->route('posts.show', $post);
}

The authorize('update', $post) call invokes PostPolicy@update with the current user and the $post instance. If it returns false, a 403 response is automatically generated.

On the Blade side:

@can('update', $post)
    <a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan

@can('delete', $post)
    <form action="{{ route('posts.destroy', $post) }}" method="POST">
        @csrf @method('DELETE')
        <button>Delete</button>
    </form>
@endcan

Gate or Policy?

When choosing between the two, I apply a simple rule:

  • If it is not tied to a specific model and involves a single check, a Gate is sufficient.
  • If a model requires checks for multiple actions (view, update, delete, publish…), a Policy is the better fit.

For areas like user management or content management, setting up a Policy consolidates all the authorization logic for that domain into a single file. When a rule changes, there is only one place to open.

Testing a Policy

Another area where this structure pays off is testability. Because a Policy is a plain class, it can be instantiated and tested directly — no need to bootstrap the controller:

public function test_kullanici_kendi_yazilarini_guncelleyebilir()
{
    $user = User::factory()->create();
    $post = Post::factory()->for($user)->create();
    $policy = new PostPolicy();

    $this->assertTrue($policy->update($user, $post));
}

public function test_kullanici_baskasinin_yazisini_guncelleyemez()
{
    $yazar  = User::factory()->create();
    $misafir = User::factory()->create();
    $post    = Post::factory()->for($yazar)->create();
    $policy  = new PostPolicy();

    $this->assertFalse($policy->update($misafir, $post));
}

Writing these tests against an if block buried in a controller is not feasible — you would be forced to test that block indirectly through an HTTP request. With a Policy, the rule is tested directly, quickly, and in isolation.

The before method: the admin bypass

Policy classes support a special method named before. This method runs before all other policy methods, and if it returns a non-null value, the other methods are never evaluated:

class PostPolicy
{
    public function before(User $user, string $ability): ?bool
    {
        if ($user->is_superadmin) {
            return true; // Super admin can do everything
        }

        return null; // Continue to normal flow
    }

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

Returning null here is critical: if you returned false, you would block everyone — including the super admin. Returning null means “I’m not making a decision; check the next method.” This is a common mistake for anyone seeing this pattern for the first time.

Before policies

Before adopting this approach, I had spread authorization checks across controllers in some places and middleware in others. The rule for who owned a model was living in three different files, each written slightly differently. With a Policy, I pulled all of that into a single class; when a rule changes, there is one file to look at. When a new role is introduced or an existing rule is relaxed, it means making the change in one place and running the tests. That confidence simply was not possible with scattered if blocks.

Confusing authentication with authorization was another early mistake. Authentication answers “who is this person?”; authorization answers “can this person do this?” In Laravel, these two layers are separate: Auth and middleware handle authentication, while Gate and Policy handle authorization. Once I clarified that distinction, it became obvious where to write what. A user may be logged in, but that does not mean they can access every resource — authenticated but unauthorized are two different states, two different layers.

Tags: #Laravel
Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

Related Posts

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind