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:
- Anyone can view comments
- Authenticated users can create comments
- Comment authors can update their own comments within 15 minutes
- Comment authors can delete their own comments
- Post authors can delete any comment on their posts
- Admins can do everything
- Return custom error messages for each denial
Exercise 2: Implement Role-Based Authorization
Create a gate-based authorization system for a blog:
- Define roles: guest, user, author, editor, admin
- Create gates: view-dashboard, create-post, publish-post, edit-any-post, delete-any-post
- Implement before/after hooks for logging
- Add middleware to protect admin routes
- Create Blade components that show/hide based on roles
Exercise 3: Advanced Policy with Custom Responses
Create a CoursePolicy for an online learning platform:
- Students can view courses they're enrolled in
- Students can enroll if they have available credits
- Instructors can update their own courses
- Return custom messages: "Insufficient credits", "Course is full", "Not enrolled"
- Use Response::denyAsNotFound() for sensitive courses
- 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.