Authorization

Laravel Intermediate/Advanced
Course 2 ยท Chapter 7 ยท Authorization

๐Ÿ›ก๏ธ Authorization

Chapter 1 answered "who are you." This chapter answers "what are you allowed to do" โ€” and here's a genuine Laravel advantage worth stating plainly: Policies are object-level by default. Django's built-in permission system is model-level by default ("can change any Book"); checking "can THIS user edit THIS specific book" normally needs a third-party package like django-guardian bolted on.

Gates: Simple, Closure-Based Checks

For an authorization rule not tied to a specific model โ€” "can this user view the admin dashboard at all" โ€” a Gate is a plain closure:

// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::define('view-admin-dashboard', function (User $user) {
        return $user->is_admin;
    });
}
if (Gate::allows('view-admin-dashboard')) { /* ... */ }
if ($request->user()->can('view-admin-dashboard')) { /* ... */ }

๐Ÿ“‹ Policies: Object-Level Authorization, By Default

php artisan make:policy BookPolicy --model=Book
// app/Policies/BookPolicy.php
namespace App\Policies;

use App\Models\Book;
use App\Models\User;

class BookPolicy
{
    public function update(User $user, Book $book): bool
    {
        return $user->id === $book->author_id;  // THIS user, THIS book โ€” genuinely object-level
    }

    public function delete(User $user, Book $book): bool
    {
        return $user->id === $book->author_id || $user->is_admin;
    }
}

Auto-discovered by naming convention โ€” BookPolicy pairs with Book automatically, no manual registration needed (in older Laravel versions, or if the naming doesn't match, it's registered explicitly in AppServiceProvider).

Django's Model-Level Default vs Laravel's Object-Level Default

Django (Built-in)

user.has_perm("catalog.change_book") โ€” a coarse, model-wide permission. It answers "can this user edit some book," not "can they edit this specific book." Per-object checks need django-guardian.

Laravel (Built-in)

BookPolicy::update(User $user, Book $book) receives the actual Book instance โ€” object-level authorization is the default shape, no additional package required.

Using Policies

// In a controller
public function update(UpdateBookRequest $request, Book $book)
{
    $this->authorize('update', $book);  // throws 403 automatically if denied

    $book->update($request->validated());
    return redirect()->route('books.show', $book);
}

Notice authorize() โ€” the exact same method name as FormRequest's authorize(): bool hook from Chapter 1's Course 1. That hook existed for exactly this purpose all along; it's the natural place to call a Policy check instead of always returning true:

// app/Http/Requests/UpdateBookRequest.php
public function authorize(): bool
{
    return $this->user()->can('update', $this->route('book'));
}

In Blade, @can/@cannot check the same policy for conditional rendering:

@can('update', $book)
  <a href="{{ route('books.edit', $book) }}">Edit</a>
@endcan

viewAny/view/create/update/delete

The conventional method names Laravel checks for automatically when a Policy is registered โ€” matching, not coincidentally, the same actions Route::resource() generates.

Policies Compose With Gate::before()

A Gate::before() closure can grant a superuser blanket access before any individual Policy method even runs โ€” a common pattern for an is_admin bypass.

๐Ÿ’ป Coding Challenges

Challenge 1: Write a Policy

Generate an AuthorPolicy for the Author model with an update() method that only allows a user to edit an author record they created (assume an owner_id column), and use $this->authorize() in a controller.

Goal: Practice writing and applying an object-level authorization check.

โ†’ Solution

Challenge 2: Use a Policy in a Blade View

Write a Blade template that shows an "Edit" link for a book only if the current user is authorized to update it, using the @can directive.

Goal: Practice conditional UI rendering based on the same Policy check used server-side.

โ†’ Solution

Challenge 3: Add an Admin Bypass With Gate::before()

Add a Gate::before() closure in AppServiceProvider that automatically grants every permission to a user with is_admin = true, and explain why this runs before individual Policy methods rather than needing to be added to each one.

Goal: Practice a global authorization override that composes with per-model Policies.

โ†’ Solution

โš ๏ธ Gotcha: authorize() Throws โ€” It Doesn't Return a Boolean

$this->authorize('update', $book) throws an AuthorizationException (automatically converted to a 403 response) when denied โ€” it does not return false the way Gate::allows() or $user->can() do. Writing if ($this->authorize(...)) is a mistake: the code inside that if never runs on denial anyway, since the exception is thrown before authorize() would ever return. Use authorize() as a standalone statement when you want a hard stop, and can()/Gate::allows() when you need an actual boolean to branch on. Separately: if a Policy's class name doesn't match its model by convention (a custom name, or a model in a non-standard namespace), it needs explicit registration in AppServiceProvider's policy mapping โ€” the same "forgot to wire it up" trap this course has hit repeatedly.

๐ŸŽฏ What's Next

The final chapter of this course covers getting everything built across both courses onto a real server: Deployment โ€” Laravel's production configuration compared to the Django course's deployment chapter.