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
| Situation | Use |
|---|---|
| A general, application-wide permission with no specific model involved | A 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 model | A Policy, even for the first rule — it scales cleanly as more actions get added |
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.
make:policy, make:controller, make:model, and more — is part of a much bigger toolkit, covered next in Artisan Console Commands.