Authorization: Gates & Policies

The previous lesson covered authentication — confirming who a user is, typically by checking a password against a hashed value in the database. Authorization is the separate question that comes after: now that Laravel knows who this user is, is this particular user actually allowed to do the thing they're trying to do? Laravel offers two complementary tools for answering that — Gates and Policies.

Gates: Simple, Closure-Based Checks

A Gate is the simplest form of authorization logic Laravel offers: a plain closure that takes the current user (and, optionally, anything else relevant) and returns true or false. Gates are best suited to actions that aren't tied to any specific model — an admin panel, a settings page, anything phrased as a general permission rather than "can this user edit this particular record". They're defined inside a service provider's boot() method, most commonly AppServiceProvider:

<?php
// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Gate;
use App\Models\User;

public function boot(): void
{
    Gate::define('view-admin-panel', function (User $user) {
        return $user->is_admin;
    });
}

Once defined, a gate can be checked from a controller with the Gate facade:

<?php
if (Gate::allows('view-admin-panel')) {
    // the currently authenticated user is allowed through
}

or, directly inside a Blade view, with the same @can/@cannot directives used for policies:

@can('view-admin-panel')
  <a href="/admin">Admin Panel</a>
@endcan

Policies: Organized Authorization for a Model

Once authorization logic starts revolving around a specific model — can this user edit this article, delete this comment — cramming every rule into AppServiceProvider as more and more gates quickly turns messy. A Policy is a dedicated class for exactly this: one class per model, with one method per action, keeping all of a model's authorization rules together in a single, predictable place. Artisan generates the skeleton for you:

php artisan make:policy ArticlePolicy --model=Article

This creates app/Policies/ArticlePolicy.php with empty methods matching common actions — view, create, update, delete — each accepting the current User plus, for actions tied to an existing record, the model instance itself:

<?php
// app/Policies/ArticlePolicy.php
public function update(User $user, Article $article): bool
{
    return $user->id === $article->author_id;
}

Recent Laravel versions auto-discover policies by naming convention, so a policy named ArticlePolicy is automatically wired up to the Article model with no further registration needed.

Checking a Policy: Controller and Blade

Inside a controller, the authorize() helper (available on any controller extending Laravel's base Controller) checks the matching policy method and, crucially, throws an automatic 403 Forbidden response if it returns false — no manual if/abort() needed:

<?php
// app/Http/Controllers/ArticleController.php
public function update(Request $request, Article $article)
{
    $this->authorize('update', $article);

    // only reaches here if $user->id === $article->author_id
    $article->update($request->validated());
}

The same check reads naturally in a Blade view, typically to hide a button the current user isn't allowed to use rather than to block a request outright:

@can('update', $article)
  <a href="/articles/{{ $article->id }}/edit">Edit</a>
@endcan

Gate or Policy? Choosing Between Them

SituationUse
A general, application-wide permission with no specific model involvedA Gate — quick to define, lives in a service provider
An action tied to one specific model instance (edit this post, delete this comment)A Policy — keeps all of a model's rules organized in one class
Several related rules for the same modelA Policy, even for the first rule — it scales cleanly as more actions get added
Note: Under the hood, both routes end up calling the same authorization machinery — Gate::define() and a Policy class are really just two different ways of registering the same kind of check, one better suited to general rules and the other to per-model rules.

Full Worked Example: Only the Original Author Can Edit

Putting the whole flow together — a policy ensuring only the article's original author can update or delete it, checked from a controller:

<?php
// app/Policies/ArticlePolicy.php
namespace App\Policies;

use App\Models\Article;
use App\Models\User;

class ArticlePolicy
{
    public function update(User $user, Article $article): bool
    {
        return $user->id === $article->author_id;
    }

    public function delete(User $user, Article $article): bool
    {
        return $user->id === $article->author_id;
    }
}
<?php
// app/Http/Controllers/ArticleController.php
public function destroy(Article $article)
{
    $this->authorize('delete', $article);

    $article->delete();

    return redirect('/articles');
}

Any other logged-in user attempting to hit this same route with someone else's article ID is automatically stopped with a 403 response, before delete() ever runs — the controller method's own logic never has to think about authorization again once the check is in place.

Next up: Every Artisan command used to generate these policies — make:policy, make:controller, make:model, and more — is part of a much bigger toolkit, covered next in Artisan Console Commands.