skip to content

In Laravel 13, how do Gate::authorize, #[Authorize], the can middleware, @can and $user->can differ in what they do when a check fails?

level: middleimportance: must knowfreq 60%

answer

  1. throwing versus returning a bool
  2. AuthorizationException becomes 403
  3. can middleware runs before the controller
  4. @can only hides markup
  5. base Controller has no authorize()

basics

~10 s

Gate::authorize, the can middleware and #[Authorize] throw AuthorizationException, which becomes a 403. $user->can and Gate::allows return a bool you must act on. @can only decides what Blade renders and protects nothing on the server.

solid answer

~40 s

All five run the same gate or policy; they differ in **when** and **what happens on denial**. `Gate::authorize('update', $article)` throws `AuthorizationException`, rendered as 403 (or the status the policy's `Response` carries). The `can` middleware (`->can('update', 'article')` or `can:update,article`) and Laravel 13's `#[Authorize('update', 'article')]` controller attribute call `Gate::authorize` **before** the action runs, after route-model binding. `$user->can('update', $article)` and `Gate::allows()` just return a boolean, so you must `abort(403)` yourself — forgetting that is a silent hole. `@can('update', $article)` wraps markup in an `if`; hiding the Edit button protects nothing if the route is unguarded. In Laravel 13 the skeleton's base `Controller` is empty, so `$this->authorize()` exists only if you add the `AuthorizesRequests` trait; `Gate::authorize()` is the default.

code

php · 17 lines
php
<?php

namespace App\Http\Controllers;

use App\Models\Article;
use Illuminate\Routing\Attributes\Controllers\Authorize;

class PublishArticleController extends Controller
{
    #[Authorize('publish', 'article')]
    public function __invoke(Article $article)
    {
        $article->update(['published_at' => now()]);

        return back();
    }
}

go deeper

for a junior

Know that Gate::authorize and the can middleware return 403 on denial, while $user->can returns a boolean and @can only hides markup.

for a middle

Explain why the can middleware sees a bound model, what #[Authorize] compiles to, and why Laravel 13 controllers lack $this->authorize().

for a senior

Make every mutating route enforce its policy server-side, use boolean checks only for behaviour, and authorize jobs with Gate::forUser.

for a principal

Set a rule the team can review mechanically: which entry point guards routes, and where boolean checks are allowed.

## Same decision, five entry points Every entry point below asks the same `Gate` service, so the same gates, policies and `before` filters decide. What changes is **when the check runs** and **what a denial does**. | Entry point | Runs | On denial | |---|---|---| | `Gate::authorize('update', $article)` | where you call it | throws `AuthorizationException` → 403 | | `->can('update', 'article')` / `can:update,article` | route middleware, before the action | throws → 403 | | `#[Authorize('update', 'article')]` | controller attribute, same middleware | throws → 403 | | `$user->can('update', $article)`, `Gate::allows()` | where you call it | returns `false`; nothing else happens | | `@can('update', $article)` | while rendering Blade | the block is not rendered | ## Throwing checks - `Gate::authorize()` returns the `Illuminate\Auth\Access\Response` when allowed and throws `AuthorizationException` when denied. The exception handler renders it as **403**, or as the status set by `Response::denyWithStatus()` / `denyAsNotFound()`. The default message is "This action is unauthorized." - The **`can` middleware** is `Illuminate\Auth\Middleware\Authorize`, aliased as `can` by default. `Route::...->can('update', 'article')` is shorthand for `can:update,article`. The second argument is a route parameter name (resolved to the bound model) or a class name for model-less abilities. - **`#[Authorize]`** (`Illuminate\Routing\Attributes\Controllers\Authorize`), new in Laravel 13, builds the same middleware from an attribute on a controller class or method, with `only` / `except` options. Because `SubstituteBindings` sits above `Authorize` in the framework's middleware priority, the article is loaded before the check runs. ## Boolean checks `$user->can()`, `$user->cannot()`, `$user->canAny()` (from the `Authorizable` trait on the skeleton's `User`), `Gate::allows()`, `Gate::denies()`, `Gate::any()` and `Gate::none()` return booleans. They are the right tool when the decision changes behaviour rather than blocking it — showing a read-only form, choosing which fields to accept. The risk is using them for protection and forgetting the `abort(403)`. ## Blade `@can`, `@cannot`, `@canany` compile to `if` statements around `Gate::check()`. They are **presentation only**: an editor who cannot see the "Publish" button can still send `POST /articles/42/publish` unless the route itself is guarded. ## Laravel 13 specifics 1. The skeleton's `App\Http\Controllers\Controller` is an empty abstract class, so `$this->authorize()` is unavailable unless you add `Illuminate\Foundation\Auth\Access\AuthorizesRequests`. 2. `#[Authorize]` joins `#[Middleware]` as a controller attribute. 3. `Gate::authorize()` is the documented in-method call. ## A publishing-platform example - `PUT /articles/{article}` guarded by `->can('update', 'article')`. - The edit page shows "Publish" inside `@can('publish', $article)`. - `POST /articles/{article}/publish` guarded by `#[Authorize('publish', 'article')]` — never by the Blade check alone. - A queued job that publishes scheduled articles calls `Gate::forUser($article->author)->authorize('publish', $article)` because no request user exists. ## Rules of thumb 1. **Guard every mutating route server-side** with `->can()`, `#[Authorize]` or `Gate::authorize()`. 2. **Prefer the route or attribute form** when the decision needs only the ability and the bound model; it is visible in `route:list` and in the controller signature. 3. **Use `Gate::authorize()` inside the method** when the decision needs data only available there, such as validated input. 4. **Use boolean checks** (`can`, `allows`) to change behaviour, never as the only protection. 5. **Use `@can` for presentation** and assume the route is guarded elsewhere. ## Reading a failure A 403 from any throwing entry point is an `AuthorizationException` carrying the policy's message, if it returned `Response::deny('...')`. For JSON requests the handler renders the message in the body; for browser requests it shows the 403 page. How that page looks is the exception handler's business, not the policy's.

  • A controller calls $this->authorize('update', $article) in a new Laravel 13 app and fails with an undefined method error. Why?
    The skeleton's base `Controller` is an empty abstract class; `authorize()` came from the `AuthorizesRequests` trait, which it no longer uses. Call `Gate::authorize('update', $article)`, use `#[Authorize]`, or add the trait back deliberately.
  • How do you run a policy check in a queued job, where there is no authenticated user?
    Name the user explicitly: `Gate::forUser($user)->authorize('publish', $article)` or `$user->can('publish', $article)`. Both build a Gate bound to that user, so the same policy and `before` filters apply without a request.

saying these in an interview costs you the question

  • Hiding a button with @can secures the route behind it
  • $user->can() throws a 403 when the check fails
  • The can middleware runs before route-model binding
  • $this->authorize() is always available in a Laravel 13 controller
  • Each entry point has its own separate set of rules