Laravel Framework

Laravel Policies & Gates

15 min Lesson 37 of 45

Laravel Policies & Gates

Authorization is a critical aspect of any application. Laravel provides two primary ways to authorize actions: Gates and Policies. Gates are simple closure-based callbacks, while Policies are classes that organize authorization logic around a particular model or resource. Let's explore both approaches in depth.

Understanding Authorization

Authorization determines whether a user is allowed to perform a specific action. Laravel's authorization features make it easy to organize your authorization logic and keep your controllers lean:

Authentication vs Authorization:
  • Authentication: Who are you? (Login, identity verification)
  • Authorization: What can you do? (Permissions, access control)
  • Laravel handles both elegantly with built-in tools

Defining Gates

Gates are simple closures that determine if a user is authorized to perform a given action. Define gates in the boot method of your AuthServiceProvider:

// app/Providers/AuthServiceProvider.php namespace App\Providers; use App\Models\Post; use App\Models\User; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Gate; class AuthServiceProvider extends ServiceProvider { /** * Register any authentication / authorization services. */ public function boot(): void { // Simple gate Gate::define('view-admin-dashboard', function (User $user) { return $user->isAdmin(); }); // Gate with additional arguments Gate::define('update-post', function (User $user, Post $post) { return $user->id === $post->user_id; }); // Gate allowing guest users (nullable User) Gate::define('view-public-content', function (?User $user) { return true; // Anyone can view }); // Complex authorization logic Gate::define('delete-post', function (User $user, Post $post) { // Owner can delete their own posts if ($user->id === $post->user_id) { return true; } // Admins can delete any post if ($user->isAdmin()) { return true; } // Moderators can delete posts in their categories if ($user->isModerator() && $user->moderatesCategory($post->category_id)) { return true; } return false; }); // Gate responses with messages Gate::define('purchase-course', function (User $user, $course) { if ($user->hasActiveMembership()) { return true; } return Gate::deny('You need an active membership to purchase courses.'); }); // Before hooks - run before all gates Gate::before(function (User $user, string $ability) { // Super admins can do everything if ($user->isSuperAdmin()) { return true; } // Don't return anything to continue normal authorization }); // After hooks - run after all gates Gate::after(function (User $user, string $ability, bool|null $result, mixed $arguments) { // Log authorization checks if ($result === false) { logger()->warning("Unauthorized access attempt", [ 'user' => $user->id, 'ability' => $ability ]); } }); } }

Using Gates

Once gates are defined, you can check them in various ways throughout your application:

// In controllers use Illuminate\Support\Facades\Gate; public function index() { // Check if user can perform action if (Gate::allows('view-admin-dashboard')) { // User can view admin dashboard } if (Gate::denies('view-admin-dashboard')) { // User cannot view admin dashboard } // Check with additional arguments $post = Post::find(1); if (Gate::allows('update-post', $post)) { // User can update this post } // Authorize or throw exception Gate::authorize('update-post', $post); // Throws AuthorizationException if denied // Check multiple abilities (any) if (Gate::any(['update-post', 'delete-post'], $post)) { // User can update OR delete } // Check multiple abilities (all) if (Gate::none(['update-post', 'delete-post'], $post)) { // User cannot update AND cannot delete } // Check for current user if (auth()->user()->can('update-post', $post)) { // Same as Gate::allows() } if (auth()->user()->cannot('update-post', $post)) { // Same as Gate::denies() } } // In routes Route::get('/admin', [AdminController::class, 'index']) ->middleware('can:view-admin-dashboard'); Route::put('/posts/{post}', [PostController::class, 'update']) ->middleware('can:update-post,post'); // In Blade templates @can('view-admin-dashboard') <a href="/admin">Admin Dashboard</a> @endcan @cannot('update-post', $post) <p>You cannot edit this post.</p> @endcannot @canany(['update-post', 'delete-post'], $post) <button>Edit or Delete</button> @endcanany
Gate vs Middleware: Use Gate::authorize() in controllers for better error messages and fine-grained control. Use middleware for route-level protection. Both approaches work together seamlessly.

Creating Policies

Policies are classes that organize authorization logic around a particular model. They're ideal for complex authorization scenarios:

// Generate a policy php artisan make:policy PostPolicy --model=Post // app/Policies/PostPolicy.php namespace App\Policies; use App\Models\Post; use App\Models\User; class PostPolicy { /** * Determine if the user can view any posts. */ public function viewAny(User $user): bool { return true; } /** * Determine if the user can view the post. */ public function view(?User $user, Post $post): bool { // Anyone can view published posts if ($post->isPublished()) { return true; } // Only owner can view draft posts return $user && $user->id === $post->user_id; } /** * Determine if the user can create posts. */ public function create(User $user): bool { // Only verified users can create posts return $user->hasVerifiedEmail(); } /** * Determine if the user can update the post. */ public function update(User $user, Post $post): bool { return $user->id === $post->user_id; } /** * Determine if the user can delete the post. */ public function delete(User $user, Post $post): bool { // Owners can delete their posts if ($user->id === $post->user_id) { return true; } // Admins can delete any post return $user->isAdmin(); } /** * Determine if the user can restore the post. */ public function restore(User $user, Post $post): bool { return $user->isAdmin(); } /** * Determine if the user can permanently delete the post. */ public function forceDelete(User $user, Post $post): bool { return $user->isAdmin(); } /** * Determine if the user can publish the post. */ public function publish(User $user, Post $post): bool { // Owner or editor can publish return $user->id === $post->user_id || $user->isEditor(); } /** * Before hook - runs before all policy methods. */ public function before(User $user, string $ability): ?bool { // Super admins can do everything if ($user->isSuperAdmin()) { return true; } // Return null to continue normal authorization return null; } }

Registering Policies

Laravel can auto-discover policies, but you can also register them manually:

// app/Providers/AuthServiceProvider.php namespace App\Providers; use App\Models\Post; use App\Models\Comment; use App\Models\User; use App\Policies\PostPolicy; use App\Policies\CommentPolicy; use App\Policies\UserPolicy; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; class AuthServiceProvider extends ServiceProvider { /** * The policy mappings for the application. */ protected $policies = [ Post::class => PostPolicy::class, Comment::class => CommentPolicy::class, User::class => UserPolicy::class, ]; public function boot(): void { // Policies will be auto-discovered or manually registered } } // Policy auto-discovery conventions: // App\Models\Post -> App\Policies\PostPolicy // App\Models\User -> App\Policies\UserPolicy // App\Models\BlogPost -> App\Policies\BlogPostPolicy
Policy Discovery: Laravel automatically discovers policies if they follow naming conventions. Place policies in app/Policies/ with the model name + "Policy" suffix. Manual registration is only needed for non-standard locations or names.

Using Policies

Policies can be checked in the same ways as gates, but with cleaner syntax:

// In controllers use App\Models\Post; public function update(Request $request, Post $post) { // Authorize using policy $this->authorize('update', $post); // If authorized, update the post $post->update($request->validated()); return redirect()->route('posts.show', $post); } public function destroy(Post $post) { // Check authorization if ($request->user()->cannot('delete', $post)) { abort(403, 'You are not authorized to delete this post.'); } $post->delete(); return redirect()->route('posts.index'); } // Alternative syntax public function edit(Post $post) { // Using Gate facade Gate::authorize('update', $post); return view('posts.edit', compact('post')); } // Check without exception public function show(Post $post) { if (auth()->user()->can('view', $post)) { // Show full post } else { // Show preview only } } // In routes - automatic model binding Route::put('/posts/{post}', [PostController::class, 'update']) ->middleware('can:update,post'); Route::delete('/posts/{post}', [PostController::class, 'destroy']) ->middleware('can:delete,post'); // Without model binding Route::post('/posts', [PostController::class, 'store']) ->middleware('can:create,App\Models\Post');

Blade Authorization Directives

Use Blade directives to show/hide content based on authorization:

<!-- Check single ability --> @can('update', $post) <a href="{{ route('posts.edit', $post) }}">Edit Post</a> @endcan @cannot('delete', $post) <p class="text-muted">You cannot delete this post</p> @endcannot <!-- Check multiple abilities (any) --> @canany(['update', 'delete'], $post) <div class="post-actions"> @can('update', $post) <button class="btn-edit">Edit</button> @endcan @can('delete', $post) <button class="btn-delete">Delete</button> @endcan </div> @endcanany <!-- Check ability without model instance --> @can('create', App\Models\Post::class) <a href="{{ route('posts.create') }}">Create New Post</a> @endcan <!-- Else clauses --> @can('update', $post) <button>Edit</button> @else <span>Read Only</span> @endcan <!-- Check if user is authenticated --> @auth <p>Welcome, {{ auth()->user()->name }}!</p> @endauth @guest <a href="{{ route('login') }}">Login</a> @endguest

Policy Methods Without Models

Some policy methods don't require a model instance (like "create"):

// In Policy public function create(User $user): bool { // Check if user can create posts return $user->hasVerifiedEmail() && !$user->isBanned(); } // In Controller public function create() { // Pass the class name $this->authorize('create', Post::class); return view('posts.create'); } // In Blade @can('create', App\Models\Post::class) <a href="{{ route('posts.create') }}">Create Post</a> @endcan // In Routes Route::get('/posts/create', [PostController::class, 'create']) ->middleware('can:create,App\Models\Post');

Authorization Responses

Provide detailed feedback when authorization fails:

use Illuminate\Auth\Access\Response; class PostPolicy { public function update(User $user, Post $post): Response { if ($user->id === $post->user_id) { return Response::allow(); } if ($post->isLocked()) { return Response::deny('This post is locked and cannot be edited.'); } return Response::deny('You do not own this post.'); } public function delete(User $user, Post $post): Response { if ($user->isBanned()) { return Response::denyWithStatus(403, 'Your account is banned.'); } if ($post->hasComments()) { return Response::denyAsNotFound(); // Return 404 instead of 403 } return $user->id === $post->user_id ? Response::allow() : Response::deny('You do not own this post.'); } } // Handle authorization exceptions // app/Exceptions/Handler.php use Illuminate\Auth\Access\AuthorizationException; public function render($request, Throwable $exception) { if ($exception instanceof AuthorizationException) { return response()->json([ 'message' => $exception->getMessage() ?: 'Unauthorized action.', ], 403); } return parent::render($request, $exception); }
Custom Messages: Use Response::deny() with custom messages to provide better UX. Users will see the exact reason why they're not authorized, rather than a generic "403 Forbidden" error.

Advanced Policy Techniques

// Checking abilities in models class Post extends Model { public function canBeEditedBy(User $user): bool { return Gate::forUser($user)->allows('update', $this); } public function canBeDeletedBy(User $user): bool { return Gate::forUser($user)->allows('delete', $this); } } // Using in code if ($post->canBeEditedBy(auth()->user())) { // Edit post } // Policy filters (bypass all checks) class PostPolicy { public function before(User $user, string $ability): ?bool { // Super admins bypass all checks if ($user->isSuperAdmin()) { return true; } // Banned users fail all checks if ($user->isBanned()) { return false; } return null; // Continue to normal policy methods } } // Authorize multiple actions at once public function bulkDelete(Request $request) { $posts = Post::findMany($request->post_ids); // Authorize all at once foreach ($posts as $post) { $this->authorize('delete', $post); } Post::destroy($posts->pluck('id')); } // Guest user policies (nullable User) public function view(?User $user, Post $post): bool { if ($post->isPublished()) { return true; // Everyone can view published posts } return $user && $user->id === $post->user_id; // Only owner can view drafts }

Exercise 1: Create a Comment Policy

Build a comprehensive CommentPolicy with the following rules:

  1. Anyone can view comments
  2. Authenticated users can create comments
  3. Comment authors can update their own comments within 15 minutes
  4. Comment authors can delete their own comments
  5. Post authors can delete any comment on their posts
  6. Admins can do everything
  7. Return custom error messages for each denial

Exercise 2: Implement Role-Based Authorization

Create a gate-based authorization system for a blog:

  1. Define roles: guest, user, author, editor, admin
  2. Create gates: view-dashboard, create-post, publish-post, edit-any-post, delete-any-post
  3. Implement before/after hooks for logging
  4. Add middleware to protect admin routes
  5. Create Blade components that show/hide based on roles

Exercise 3: Advanced Policy with Custom Responses

Create a CoursePolicy for an online learning platform:

  1. Students can view courses they're enrolled in
  2. Students can enroll if they have available credits
  3. Instructors can update their own courses
  4. Return custom messages: "Insufficient credits", "Course is full", "Not enrolled"
  5. Use Response::denyAsNotFound() for sensitive courses
  6. Implement a before() method for suspended accounts

Summary

In this lesson, you mastered Laravel's authorization system using Gates and Policies. You learned how to define simple gates for quick checks, create comprehensive policies for model-based authorization, register and auto-discover policies, use authorization in controllers, routes, and Blade views, and provide detailed authorization responses. These tools enable you to build secure applications with fine-grained access control.

In the next lesson, we'll explore advanced queue features including job batching, chaining, and handling failures.