Originally published at hafiz.dev
Authentication tells Laravel who you are. Authorization tells Laravel what you're allowed to do. Most developers get authentication right from day one. Laravel's starter kits handle it. Authorization is the part that quietly goes wrong. Rules end up scattered across controllers, Blade files, and middleware, duplicated in three places, and inconsistently applied. One controller checks a Gate. Another skips the check entirely. A third checks directly against a column value inline. After a year of that, nobody knows for certain whether a given action is actually protected.
Laravel solves this with two tools: Gates and Policies. They look similar, they work differently, and knowing which to reach for saves you from that maintenance mess. This guide covers both: what they do, when to use each, how to wire them up correctly, and the specific mistakes worth avoiding.
Gates vs Policies: The One-Sentence Version
Gates are closures for authorization checks that don't belong to a model. Policies are classes that organize authorization logic around a specific model. That's the whole distinction. The rest is just details.
The Laravel docs use a good analogy: Gates are to routes as Policies are to controllers. A Gate is a quick closure you define in AppServiceProvider. A Policy is a dedicated class with methods for every action a user might take against a resource. When your app is small, Gates feel faster. When your app grows, Policies scale much better because the logic is organized, testable in isolation, and easy to find when something needs to change.
You don't have to pick one. Most real applications use both. Gates for cross-cutting concerns like "can this user access the admin panel", Policies for resource-specific logic like "can this user edit this post". The decision tree is simple: if the check involves an Eloquent model, use a Policy. If it doesn't, a Gate is probably fine.
Gates: Quick Authorization Without a Model
Define Gates in the boot() method of App\Providers\AppServiceProvider:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::define('view-admin-dashboard', function (User $user): bool {
return $user->is_admin;
});
Gate::define('manage-settings', function (User $user): bool {
return $user->hasRole('super-admin');
});
}
Laravel automatically injects the authenticated user as the first argument. You never pass it manually. Then anywhere in your application you check it with Gate::allows() or Gate::denies():
use Illuminate\Support\Facades\Gate;
// In a controller
if (Gate::denies('view-admin-dashboard')) {
abort(403);
}
// Throw automatically if denied
Gate::authorize('manage-settings');
In Blade templates, @can and @cannot do the same job without touching PHP:
@can('view-admin-dashboard')
<a href="/admin">Admin Panel</a>
@endcan
When to use Gates:
- Authorization checks that aren't tied to a specific Eloquent model
- Global permissions like admin access, beta feature toggles, or subscription tier checks
- Simple one-off checks that would be over-engineered as a full Policy class
- Cross-cutting checks that apply across multiple models or resources
When not to use Gates: The moment you find yourself passing a model instance to a Gate definition, stop. That's a Policy.
Policies: Authorization Organized Around a Model
A Policy is a class with one method per action a user can take on a resource. Generate one with:
php artisan make:policy PostPolicy --model=Post
The --model flag populates the class with the standard methods: viewAny, view, create, update, delete, restore, and forceDelete. You fill in the logic:
<?php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class PostPolicy
{
public function viewAny(User $user): bool
{
return true; // any authenticated user can list posts
}
public function view(User $user, Post $post): bool
{
return $post->published || $user->id === $post->user_id;
}
public function create(User $user): bool
{
return $user->email_verified_at !== null;
}
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;
}
}
Notice viewAny and create don't take a Post instance. There's no specific post to check against yet. view, update, and delete do, because they operate on a specific model.
Registering Policies: Three Ways in Laravel 13
Auto-discovery (Laravel 13 default): If your PostPolicy lives in app/Policies/ and your Post model is in app/Models/, Laravel finds the connection automatically through naming conventions. You don't register anything. This is the default for new Laravel 13 projects and works for the vast majority of cases.
Manual registration in AppServiceProvider:
use App\Models\Post;
use App\Policies\PostPolicy;
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::policy(Post::class, PostPolicy::class);
}
Use this when the naming convention doesn't match, for example if your policy is named ArticlePolicy but the model is Post.
The #[UsePolicy] attribute (Laravel 13): You can declare the policy directly on the model class using a PHP attribute:
<?php
namespace App\Models;
use App\Policies\PostPolicy;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Model;
#[UsePolicy(PostPolicy::class)]
class Post extends Model
{
//
}
This is the most explicit option. The relationship between the model and its policy is visible right where the model is defined, without jumping to AppServiceProvider. It's worth using if your codebase has a lot of non-standard naming, or if you simply value that explicitness. You can find this and all the other make:policy and related Artisan commands in the Laravel Artisan Commands reference.
How to Call a Policy
There are four places where you can trigger a policy check. Each has the right use case.
1. In the controller with Gate::authorize(): This is the most common pattern in Laravel 13. It throws a 403 exception automatically if the check fails:
use Illuminate\Support\Facades\Gate;
public function update(Request $request, Post $post): RedirectResponse
{
Gate::authorize('update', $post);
// Only runs if the user passed the policy check
$post->update($request->validated());
return redirect()->route('posts.index');
}
2. On the route with ->can() middleware: Good for protecting an entire route before it even reaches the controller. Works especially well with implicit model binding:
Route::put('/posts/{post}', [PostController::class, 'update'])
->can('update', 'post');
Route::post('/posts', [PostController::class, 'store'])
->can('create', Post::class);
3. Via the #[Authorize] attribute on controller methods (Laravel 13): Clean and declarative if you're already using PHP attributes on your controllers:
use Illuminate\Routing\Attributes\Controllers\Authorize;
#[Authorize('update', 'post')]
public function update(Post $post): RedirectResponse
{
// ...
}
4. The authorizeResource() shortcut for resource controllers: One call in the constructor wires up authorization for all seven resource methods automatically. This is the most efficient option when you have a full resource controller paired with a matching Policy:
public function __construct()
{
$this->authorizeResource(Post::class, 'post');
}
This maps index → viewAny, show → view, create/store → create, edit/update → update, destroy → delete. The second argument ('post') tells Laravel which route parameter to resolve for model binding. If you have a full resource controller with a corresponding Policy, this is the cleanest option and removes a lot of repeated Gate::authorize() calls from individual methods.
One setup step required in Laravel 11+: The slim base controller in Laravel 11, 12, and 13 no longer includes authorizeResource() by default. To use it, add the AuthorizesRequests trait to your base controller at app/Http/Controllers/Controller.php:
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Routing\Controller as BaseController;
abstract class Controller extends BaseController
{
use AuthorizesRequests;
}
Once the trait is in place, authorizeResource() works as expected in any controller that extends Controller. If you'd rather not touch the base controller, the ->can() route middleware is the cleanest alternative and requires no trait at all.
This maps index → viewAny, show → view, create/store → create, edit/update → update, destroy → delete. The second argument ('post') tells Laravel which route parameter to resolve for model binding. If you have a full resource controller with a corresponding Policy, this is the cleanest option and removes a lot of repeated Gate::authorize() calls from individual methods.
The Policy before() Method: Super-Admin Shortcut
The before() method runs before every other method in a Policy. Use it to grant blanket access without touching every individual method:
public function before(User $user, string $ability): bool|null
{
if ($user->is_super_admin) {
return true;
}
return null; // null means "proceed to the normal method"
}
Returning true grants access immediately. Returning false denies it immediately. Returning null (or not returning anything) falls through to the individual policy method. This pattern keeps admin bypass logic in one place instead of scattered across every method.
Important: before() is on the Policy, not the Gate. A similar Gate::before() exists at the Gate level and intercepts all Gate and Policy checks globally. Use Gate::before() sparingly. It applies everywhere, which can create confusing behaviour if a check somewhere expects a denial but a global before() keeps returning true.
Returning Rich Responses Instead of Booleans
Policy methods don't have to return bare booleans. You can return a Response object that includes a denial message. This is useful for APIs where the client needs to know why a request was rejected:
use Illuminate\Auth\Access\Response;
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::deny('You do not own this post.');
}
When using Gate::inspect() instead of Gate::authorize(), you can retrieve the full response including the message:
$response = Gate::inspect('update', $post);
if ($response->denied()) {
return response()->json(['message' => $response->message()], 403);
}
This pairs well with Laravel REST API patterns where the client needs to display the reason for rejection, not just a generic 403.
Authorization in Blade
The @can and @cannot directives work with both Gates and Policies. For Policies, pass the model instance:
{{-- Gate check --}}
@can('view-admin-dashboard')
<a href="/admin">Admin</a>
@endcan
{{-- Policy check with model --}}
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan
@cannot('delete', $post)
<span class="text-muted">You can't delete this post</span>
@endcannot
One thing to remember: Blade directives only hide UI. They don't secure the underlying routes. Always check authorization server-side too. A user could navigate directly to /posts/1/edit and bypass the hidden button completely.
Authorization and Inertia: Sharing Policy Data with the Frontend
If you're building with Inertia.js, your Vue or React components often need to know what the current user can do in order to show or hide UI elements. The cleanest pattern is sharing authorization data through Inertia's shared props in HandleInertiaRequests middleware:
public function share(Request $request): array
{
return [
...parent::share($request),
'can' => [
'create_post' => $request->user()?->can('create', Post::class),
'manage_settings' => $request->user()?->can('manage-settings'),
],
];
}
Your frontend then reads $page.props.can.create_post without making a separate API request. This approach is covered in the Inertia.js v3 upgrade guide for teams migrating their frontend setup, and it pairs well with the route-level ->can() middleware handling the actual enforcement server-side.
Keep the can object lean. Only share what the current page's UI actually needs. Sharing every possible permission for every resource is wasteful and leaks your authorization surface to the client.
The Mistakes Worth Knowing
Checking authorization only in Blade. The most common mistake. Hiding a button doesn't protect the endpoint. Always pair Blade @can checks with server-side Gate::authorize() or the can middleware. A determined user can navigate directly to any URL regardless of what your Blade template shows them.
Authorizing inside Eloquent models. Authorization logic doesn't belong in a model's events or observers. It belongs at the request layer: controller, middleware, or route. By the time a model method runs, the authorization window has passed and you've lost the request context you need to make the check meaningful.
One giant Gate definition file. If you have 40 Gate::define() calls in AppServiceProvider, that's a sign you should be using Policies. Gates are for the handful of non-model checks. Anything model-specific belongs in a Policy class.
Forgetting viewAny vs view. viewAny authorizes the index action: listing all records. view authorizes reading a specific instance. Developers often only write view and then wonder why their index route isn't protected. With authorizeResource(), both get wired up automatically.
Calling $this->authorize() in Laravel 13 controllers. In older Laravel versions, controllers extended a base class that provided a $this->authorize() helper method. In Laravel 13's leaner controller structure, the recommended approach is Gate::authorize() directly. Both work, but Gate::authorize() doesn't depend on inheriting from the base controller class.
Returning false from before() when you mean to skip. The before() method returns null to "pass through" to the normal policy method, not false. Returning false explicitly denies the action. This trips developers up because the instinct is to return false when you don't want before() to take effect.
When Spatie Permission Fits In
Spatie's spatie/laravel-permission package gives you database-driven roles and permissions rather than hardcoded authorization logic. It's the right addition when your application needs admins to assign and revoke permissions without a code deploy. The package stores roles and permissions in the database, so a super-admin can grant edit-any-post to the editor role through a UI without touching any PHP.
It integrates directly with Policies via the can() check:
public function update(User $user, Post $post): bool
{
// owner can always edit their own post
if ($user->id === $post->user_id) {
return true;
}
// editors with the right permission can edit anything
return $user->can('edit-any-post');
}
The rule of thumb: use Gates and Policies for logic that's structural and fixed (owners can edit their own posts, admins can see the dashboard). Add Spatie Permission when the business rules themselves need to be configurable at runtime. The combination of both is covered in depth in the complete Laravel + Filament SaaS guide, which uses role-based access in a real admin panel context.
Frequently Asked Questions
Do I need to register a Policy manually in Laravel 13?
No, not for standard naming conventions. If your PostPolicy is in app/Policies/ and your Post model is in app/Models/, auto-discovery handles it. Only register manually via Gate::policy() or #[UsePolicy] when naming diverges from the convention.
What's the difference between Gate::allows() and Gate::authorize()?
Gate::allows() returns a boolean, useful when you want to branch logic. Gate::authorize() throws an AuthorizationException (HTTP 403) if denied, which stops execution immediately. Use allows() for conditional logic, authorize() for enforcement.
Can a Policy method allow unauthenticated users?
By default, all policy and gate checks return false for unauthenticated requests. To allow guest access on a specific policy method, make the $user argument nullable:
public function view(?User $user, Post $post): bool
{
return $post->published || optional($user)->id === $post->user_id;
}
Can I authorize multiple models in one Policy method?
Yes. Pass them as an array to the second argument:
Gate::authorize('update', [$post, $category]);
Then accept both in the policy method:
public function update(User $user, Post $post, Category $category): bool
{
return $user->id === $post->user_id
&& $user->managedCategories->contains($category);
}
Where should I put authorization logic for API routes?
Same place as web routes: controller-level Gate::authorize() or route-level ->can() middleware. The can middleware works identically on API routes. The difference is in the response: a 403 JSON response rather than a redirect, which Laravel handles automatically for requests that accept JSON.
Authorization done right is invisible. Users can only see and do what they're supposed to, and you never have to think about it again. The mistake is deferring it, scattering checks across controllers, duplicating logic, and ending up with authorization that's inconsistently applied. Starting with Policies from day one, even on a project that feels small, costs almost nothing and pays back every time you add a feature or onboard a developer who needs to understand what the app allows.
Gates for the non-model checks. Policies for everything tied to an Eloquent model. authorizeResource() when you have a full resource controller. That's the mental model that keeps a Laravel app secure and maintainable at any scale.
Building something and not sure how to structure the authorization layer? Get in touch and I'm happy to take a look.
Top comments (0)