Laravel Policies and Gates: Authorization Done Right

Gates and Policies both handle authorization in Laravel, but they suit different situations. Here's when to use each, and how to keep permission logic organized.

Two Tools, Different Jobs

Gates and Policies both answer the question “is this user allowed to do this,” but they’re suited to different situations. Understanding when to reach for each — and how they work under the hood — keeps authorization logic organized as an application grows past a handful of simple permission checks.

Gates: Simple, Closure-Based Authorization

A Gate is essentially a closure registered with a name, typically defined in a service provider: Gate::define('view-admin-panel', fn($user) => $user->is_admin). Gates are well suited to authorization checks that aren’t tied to a specific Eloquent model — permissions around a general feature or section of your application rather than a particular resource instance.

Policies: Model-Centric Authorization

A Policy is a class dedicated to authorization logic for a specific model, with methods corresponding to actions: view, update, delete, and any custom actions your application needs. Laravel automatically resolves the correct policy for a given model when you call $user->can('update', $post), based on naming convention or explicit registration — this convention-based resolution is one of the more elegant pieces of Laravel’s authorization system.

Writing a Policy, Step by Step

Running php artisan make:policy PostPolicy --model=Post scaffolds a policy class with stub methods for common actions. Inside update(User $user, Post $post), you write the actual business logic — commonly something like checking whether the user owns the post, or holds a role that grants broader edit permissions. Keeping this logic centralized in the policy, rather than scattered as inline conditionals across multiple controllers, is the entire point: one place to look, one place to change, one place to test.

Authorizing in Controllers

The authorize() method, available in controllers via the AuthorizesRequests trait, throws an AuthorizationException automatically if a check fails, which Laravel converts into a 403 response — no manual if-statement-then-abort boilerplate needed in every controller method. This keeps controllers focused on orchestrating the request rather than tangled with authorization branching logic.

Authorizing in Blade Templates

The @can and @cannot Blade directives let you conditionally show UI elements based on the same policy logic used server-side — hiding an edit button for users who can’t actually edit a resource. This is a UX nicety, not a security boundary on its own; the actual enforcement still needs to happen server-side in the controller or route, since a hidden button doesn’t prevent a direct request to the underlying endpoint.

Policy Responses with Custom Messages

Rather than a bare true/false, policy methods can return a Response::deny('You must verify your email first.'), which surfaces a specific, meaningful error message to the user instead of a generic “not authorized.” This small feature meaningfully improves user experience for authorization failures that have a clear, actionable reason behind them.

Before Hooks: Handling Super-Admin Logic Cleanly

Rather than repeating “unless the user is a super-admin” in every single policy method, a before() method on a policy runs before any other check and can short-circuit the entire policy with a definitive true or false. This is the correct place for blanket admin-override logic — write it once, and every method on that policy automatically respects it without duplication.

Testing Authorization Logic

Policies are straightforward to unit test directly, without needing to go through an HTTP request at all — instantiate the policy, call the method with a user and a model instance, and assert the expected boolean or response. This is significantly faster than testing authorization purely through feature tests hitting real routes, and it isolates authorization bugs from unrelated route or middleware issues.

Common Mistakes to Avoid

  • Relying only on frontend hiding of buttons or menu items without a corresponding server-side check — the actual vulnerability that matters.
  • Scattering ad-hoc authorization conditionals across controllers instead of centralizing logic in policies, making it hard to audit who can do what.
  • Forgetting to register a policy for a model, causing Laravel to silently fall through to a default “unauthorized” response that can be confusing to debug without checking the policy registration first.

The Practical Payoff

A well-organized authorization layer, built consistently with Gates for general checks and Policies for model-specific ones, makes a codebase dramatically easier to audit for security correctness. When every authorization decision lives in a predictable, well-known location rather than scattered inline throughout the application, reviewing “who can do what” becomes a tractable exercise instead of an archaeological dig through controller logic written by different developers at different times.