Laravel Authorization with Gates and Policies: Controlling User Access

Published on November 20, 2025
Laravel Authorization Gates Policies Security AccessControl

Introduction to Laravel Authorization

Authorization determines what authenticated users are allowed to do. While authentication answers "Who are you?", authorization answers "What are you allowed to do?"

Two Main Approaches:

  • Gates: Closure-based authorization for simple, general actions
  • Policies: Class-based authorization for model-specific actions

Understanding Gates

What are Gates?

Gates are closure-based authorization rules that define abilities across your application. They're perfect for authorization that doesn't require a specific model.

Defining Gates

Basic Gate Registration
<?php
// app/Providers/AuthServiceProvider.php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use App\Models\User;

class AuthServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Basic gate - check if user is admin
        Gate::define('view-admin-dashboard', function (User $user) {
            return $user->is_admin === true;
        });

        // Gate with parameters
        Gate::define('update-settings', function (User $user, string $section) {
            if ($user->is_admin) {
                return true;
            }
            
            return in_array($section, ['profile', 'notifications']);
        });

        // Gate before - runs before all other gates
        Gate::before(function (User $user, $ability) {
            if ($user->is_super_admin) {
                return true;
            }
        });

        // Gate after - runs after all other gates
        Gate::after(function (User $user, $ability, $result, $arguments) {
            if ($user->is_super_admin) {
                return true;
            }
        });
    }
}
Advanced Gate Examples
// app/Providers/AuthServiceProvider.php

public function boot(): void
{
    // Check if user can manage users
    Gate::define('manage-users', function (User $user) {
        return $user->hasRole('admin') || $user->hasRole('moderator');
    });

    // Check if user can access billing
    Gate::define('access-billing', function (User $user) {
        return $user->subscribed() || $user->onTrial();
    });

    // Check if user can export data
    Gate::define('export-data', function (User $user, $dataType) {
        if (!$user->hasPermission('export')) {
            return false;
        }

        $allowedTypes = match($user->role) {
            'admin' => ['users', 'orders', 'products', 'analytics'],
            'manager' => ['orders', 'products'],
            default => ['orders'],
        };

        return in_array($dataType, $allowedTypes);
    });

    // Time-based gate (only during business hours)
    Gate::define('access-live-chat', function (User $user) {
        $now = now();
        $isBusinessHours = $now->isWeekday() && 
                          $now->between('09:00', '17:00');
        
        return $user->hasSupportAccess() && $isBusinessHours;
    });

    // Resource limit gate
    Gate::define('create-project', function (User $user) {
        $maxProjects = match($user->plan) {
            'premium' => 50,
            'professional' => 20,
            'basic' => 5,
            default => 1,
        };

        return $user->projects()->count() < $maxProjects;
    });
}

Using Gates

In Controllers

<?php
// app/Http/Controllers/AdminController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;

class AdminController extends Controller
{
    public function dashboard(Request $request)
    {
        // Check authorization
        if (!Gate::allows('view-admin-dashboard')) {
            abort(403);
        }

        return view('admin.dashboard');
    }

    public function settings(Request $request, $section)
    {
        // Check authorization with parameters
        if (!Gate::allows('update-settings', $section)) {
            abort(403, "You cannot update {$section} settings.");
        }

        return view("settings.{$section}");
    }

    // Using authorize method (throws AuthorizationException)
    public function exportData(Request $request, $dataType)
    {
        $this->authorize('export-data', $dataType);

        // Export logic here
        return $this->export($dataType);
    }

    // Multiple authorization checks
    public function manageUsers(Request $request)
    {
        Gate::authorize('manage-users');
        Gate::authorize('access-admin-panel');

        return view('admin.users');
    }
}

In Blade Templates

{{-- Basic gate check --}}
@can('view-admin-dashboard')
    <a href="/admin/dashboard" class="nav-link">Admin Dashboard</a>
@endcan

{{-- Gate with parameters --}}
@can('update-settings', 'billing')
    <a href="/settings/billing" class="nav-link">Billing Settings</a>
@endcan

{{-- Alternative syntax --}}
@if (Gate::allows('manage-users'))
    <div class="admin-panel">
        <h3>User Management</h3>
        <!-- User management interface -->
    </div>
@endif

{{-- Multiple conditions --}}
@can('access-billing')
    @can('export-data', 'invoices')
        <button onclick="exportInvoices()">Export Invoices</button>
    @endcan
@endcan

{{-- Else condition --}}
@can('create-project')
    <button>Create New Project</button>
@else
    <p class="text-muted">Upgrade your plan to create more projects.</p>
@endcan

In API Routes

// routes/api.php

Route::middleware('auth:sanctum')->group(function () {
    // Using gate in route middleware
    Route::get('/admin/metrics', function (Request $request) {
        Gate::authorize('view-admin-dashboard');
        
        return response()->json([
            'metrics' => $this->getAdminMetrics()
        ]);
    });

    Route::get('/exports/{type}', function (Request $request, $type) {
        if (!Gate::check('export-data', $type)) {
            return response()->json([
                'error' => 'Unauthorized to export this data type.'
            ], 403);
        }

        return $this->generateExport($type);
    });
});

Understanding Policies

What are Policies?

Policies are class-based authorization rules that organize authorization logic around a particular model or resource. They're perfect for CRUD operations and model-specific permissions.

Creating Policies

Generating Policies
# Create policy for Post model
php artisan make:policy PostPolicy --model=Post

# Create policy for multiple models
php artisan make:policy UserPolicy --model=User
php artisan make:policy CommentPolicy --model=Comment

# Create policy without model
php artisan make:policy ReportPolicy
Policy Auto-Discovery

Laravel automatically discovers policies if you follow naming conventions:

  • Post model → PostPolicy class
  • User model → UserPolicy class

Complete Policy Examples

Post Policy

<?php
// app/Policies/PostPolicy.php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    /**
     * Determine whether the user can view any models.
     */
    public function viewAny(User $user): bool
    {
        // Anyone can view posts list
        return true;
    }

    /**
     * Determine whether the user can view the model.
     */
    public function view(User $user, Post $post): bool
    {
        // Users can view their own posts or published posts
        return $post->user_id === $user->id || $post->is_published;
    }

    /**
     * Determine whether the user can create models.
     */
    public function create(User $user): bool
    {
        // Users must be verified to create posts
        return $user->hasVerifiedEmail() && $user->can_create_posts;
    }

    /**
     * Determine whether the user can update the model.
     */
    public function update(User $user, Post $post): bool
    {
        // Users can update their own posts
        // Admins can update any post
        return $user->id === $post->user_id || $user->is_admin;
    }

    /**
     * Determine whether the user can delete the model.
     */
    public function delete(User $user, Post $post): bool
    {
        // Users can delete their own posts
        // Admins can delete any post except their own (prevent accidental deletion)
        return $user->id === $post->user_id || 
               ($user->is_admin && $user->id !== $post->user_id);
    }

    /**
     * Determine whether the user can restore the model.
     */
    public function restore(User $user, Post $post): bool
    {
        // Only admins can restore soft deleted posts
        return $user->is_admin;
    }

    /**
     * Determine whether the user can permanently delete the model.
     */
    public function forceDelete(User $user, Post $post): bool
    {
        // Only super admins can permanently delete
        return $user->is_super_admin;
    }

    /**
     * Determine whether the user can publish the post.
     */
    public function publish(User $user, Post $post): bool
    {
        // Users can publish their own drafts
        // Admins can publish any draft
        return $post->user_id === $user->id || $user->is_admin;
    }

    /**
     * Determine whether the user can feature the post.
     */
    public function feature(User $user, Post $post): bool
    {
        // Only editors and admins can feature posts
        return $user->hasRole('editor') || $user->is_admin;
    }

    /**
     * Determine whether the user can view post analytics.
     */
    public function viewAnalytics(User $user, Post $post): bool
    {
        // Post owners and admins can view analytics
        return $post->user_id === $user->id || $user->is_admin;
    }
}

User Policy (for managing users)

<?php
// app/Policies/UserPolicy.php

namespace App\Policies;

use App\Models\User;

class UserPolicy
{
    /**
     * Determine whether the user can view any models.
     */
    public function viewAny(User $user): bool
    {
        // Only admins and HR can view user list
        return $user->is_admin || $user->hasRole('hr');
    }

    /**
     * Determine whether the user can view the model.
     */
    public function view(User $user, User $model): bool
    {
        // Users can view their own profile
        // Admins can view any profile
        return $user->id === $model->id || $user->is_admin;
    }

    /**
     * Determine whether the user can create models.
     */
    public function create(User $user): bool
    {
        // Only admins can create new users
        return $user->is_admin;
    }

    /**
     * Determine whether the user can update the model.
     */
    public function update(User $user, User $model): bool
    {
        // Users can update their own profile
        // Admins can update any profile except their own role
        if ($user->id === $model->id) {
            return true;
        }

        return $user->is_admin && $user->id !== $model->id;
    }

    /**
     * Determine whether the user can delete the model.
     */
    public function delete(User $user, User $model): bool
    {
        // Admins can delete users except themselves and other admins
        return $user->is_admin && 
               $user->id !== $model->id && 
               !$model->is_admin;
    }

    /**
     * Determine whether the user can restore the model.
     */
    public function restore(User $user, User $model): bool
    {
        return $user->is_admin;
    }

    /**
     * Determine whether the user can permanently delete the model.
     */
    public function forceDelete(User $user, User $model): bool
    {
        // Only super admins can permanently delete users
        return $user->is_super_admin;
    }

    /**
     * Determine whether the user can change user roles.
     */
    public function changeRole(User $user, User $model): bool
    {
        // Only admins can change roles, and cannot change their own role
        return $user->is_admin && $user->id !== $model->id;
    }

    /**
     * Determine whether the user can impersonate another user.
     */
    public function impersonate(User $user, User $model): bool
    {
        // Only admins can impersonate, and cannot impersonate other admins
        return $user->is_admin && 
               $user->id !== $model->id && 
               !$model->is_admin;
    }

    /**
     * Determine whether the user can view user analytics.
     */
    public function viewAnalytics(User $user, User $model): bool
    {
        // Users can view their own analytics, admins can view all
        return $user->id === $model->id || $user->is_admin;
    }
}

Using Policies

In Controllers

<?php
// app/Http/Controllers/PostController.php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;

class PostController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index(Request $request)
    {
        // Policy automatically checks viewAny
        $this->authorize('viewAny', Post::class);

        $posts = Post::with('user')->latest()->paginate(10);

        return view('posts.index', compact('posts'));
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        $this->authorize('create', Post::class);

        return view('posts.create');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request)
    {
        $this->authorize('create', Post::class);

        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'content' => 'required|string',
        ]);

        $post = $request->user()->posts()->create($validated);

        return redirect()->route('posts.show', $post);
    }

    /**
     * Display the specified resource.
     */
    public function show(Post $post)
    {
        // Automatically uses view policy
        $this->authorize('view', $post);

        return view('posts.show', compact('post'));
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(Post $post)
    {
        $this->authorize('update', $post);

        return view('posts.edit', compact('post'));
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, Post $post)
    {
        $this->authorize('update', $post);

        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'content' => 'required|string',
        ]);

        $post->update($validated);

        return redirect()->route('posts.show', $post);
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Post $post)
    {
        $this->authorize('delete', $post);

        $post->delete();

        return redirect()->route('posts.index');
    }

    /**
     * Publish a post.
     */
    public function publish(Post $post)
    {
        $this->authorize('publish', $post);

        $post->update(['is_published' => true]);

        return redirect()->route('posts.show', $post)
                       ->with('success', 'Post published successfully!');
    }

    /**
     * Feature a post.
     */
    public function feature(Post $post)
    {
        $this->authorize('feature', $post);

        $post->update(['is_featured' => true]);

        return redirect()->route('posts.show', $post)
                       ->with('success', 'Post featured successfully!');
    }
}

In Blade Templates

{{-- Basic policy checks --}}
@can('create', App\Models\Post::class)
    <a href="{{ route('posts.create') }}" class="btn btn-primary">
        Create Post
    </a>
@endcan

@foreach($posts as $post)
    <div class="post-card">
        <h3>{{ $post->title }}</h3>
        <p>{{ $post->excerpt }}</p>
        
        {{-- Policy for specific post --}}
        @can('view', $post)
            <a href="{{ route('posts.show', $post) }}">Read More</a>
        @endcan
        
        {{-- Multiple policy checks --}}
        <div class="post-actions">
            @can('update', $post)
                <a href="{{ route('posts.edit', $post) }}" class="btn btn-sm btn-outline-secondary">
                    Edit
                </a>
            @endcan
            
            @can('delete', $post)
                <form action="{{ route('posts.destroy', $post) }}" method="POST" class="d-inline">
                    @csrf
                    @method('DELETE')
                    <button type="submit" class="btn btn-sm btn-outline-danger" 
                            onclick="return confirm('Are you sure?')">
                        Delete
                    </button>
                </form>
            @endcan
            
            @can('publish', $post)
                @if(!$post->is_published)
                    <form action="{{ route('posts.publish', $post) }}" method="POST" class="d-inline">
                        @csrf
                        <button type="submit" class="btn btn-sm btn-outline-success">
                            Publish
                        </button>
                    </form>
                @endif
            @endcan
            
            @can('feature', $post)
                @if(!$post->is_featured)
                    <form action="{{ route('posts.feature', $post) }}" method="POST" class="d-inline">
                        @csrf
                        <button type="submit" class="btn btn-sm btn-outline-warning">
                            Feature
                        </button>
                    </form>
                @endif
            @endcan
        </div>
    </div>
@endforeach

{{-- Policy checks without else --}}
@can('viewAnalytics', $post)
    <div class="analytics-section">
        <h4>Post Analytics</h4>
        <p>Views: {{ $post->views }}</p>
        <p>Engagement: {{ $post->engagement_rate }}%</p>
    </div>
@endcan

In API Routes

// routes/api.php

Route::middleware('auth:sanctum')->group(function () {
    // Post routes with policy authorization
    Route::get('/posts', function (Request $request) {
        // Check viewAny policy
        if ($request->user()->cannot('viewAny', Post::class)) {
            return response()->json(['error' => 'Unauthorized'], 403);
        }

        return Post::with('user')->latest()->paginate(10);
    });

    Route::post('/posts', function (Request $request) {
        $request->user()->authorize('create', Post::class);

        $post = $request->user()->posts()->create(
            $request->validate([
                'title' => 'required|string|max:255',
                'content' => 'required|string',
            ])
        );

        return response()->json($post, 201);
    });

    Route::put('/posts/{post}', function (Request $request, Post $post) {
        $request->user()->authorize('update', $post);

        $post->update(
            $request->validate([
                'title' => 'sometimes|string|max:255',
                'content' => 'sometimes|string',
            ])
        );

        return response()->json($post);
    });

    Route::delete('/posts/{post}', function (Request $request, Post $post) {
        $request->user()->authorize('delete', $post);

        $post->delete();

        return response()->json(['message' => 'Post deleted']);
    });
});

Advanced Authorization Scenarios

Team-Based Authorization

<?php
// app/Policies/ProjectPolicy.php

namespace App\Policies;

use App\Models\Project;
use App\Models\User;

class ProjectPolicy
{
    /**
     * Determine whether the user can view any models.
     */
    public function viewAny(User $user): bool
    {
        // Users can view projects from their teams
        return $user->teams()->exists();
    }

    /**
     * Determine whether the user can view the model.
     */
    public function view(User $user, Project $project): bool
    {
        // User must be member of project's team
        return $user->teams->contains($project->team_id);
    }

    /**
     * Determine whether the user can create models.
     */
    public function create(User $user): bool
    {
        // User must be team owner or admin to create projects
        return $user->ownedTeams()->exists() || $user->is_admin;
    }

    /**
     * Determine whether the user can update the model.
     */
    public function update(User $user, Project $project): bool
    {
        // Team owners and project managers can update
        return $project->team->owner_id === $user->id ||
               $project->manager_id === $user->id ||
               $user->is_admin;
    }

    /**
     * Determine whether the user can delete the model.
     */
    public function delete(User $user, Project $project): bool
    {
        // Only team owners and admins can delete
        return $project->team->owner_id === $user->id || $user->is_admin;
    }

    /**
     * Determine whether the user can manage project members.
     */
    public function manageMembers(User $user, Project $project): bool
    {
        return $project->team->owner_id === $user->id ||
               $project->manager_id === $user->id ||
               $user->hasRole('project_lead');
    }

    /**
     * Determine whether the user can view project budget.
     */
    public function viewBudget(User $user, Project $project): bool
    {
        return $project->team->owner_id === $user->id ||
               $project->manager_id === $user->id ||
               $user->hasRole('finance');
    }
}

Time-Based Authorization

<?php
// app/Policies/TimeEntryPolicy.php

namespace App\Policies;

use App\Models\TimeEntry;
use App\Models\User;
use Carbon\Carbon;

class TimeEntryPolicy
{
    /**
     * Determine whether the user can create models.
     */
    public function create(User $user): bool
    {
        // Can only create entries during work hours
        $now = now();
        return $now->isWeekday() && $now->between('06:00', '20:00');
    }

    /**
     * Determine whether the user can update the model.
     */
    public function update(User $user, TimeEntry $timeEntry): bool
    {
        // Can only update recent entries (within 7 days)
        return $timeEntry->created_at->gte(now()->subDays(7)) &&
               $user->id === $timeEntry->user_id;
    }

    /**
     * Determine whether the user can delete the model.
     */
    public function delete(User $user, TimeEntry $timeEntry): bool
    {
        // Can only delete if not approved and within 24 hours
        return !$timeEntry->is_approved &&
               $timeEntry->created_at->gte(now()->subDay()) &&
               $user->id === $timeEntry->user_id;
    }

    /**
     * Determine whether the user can approve time entries.
     */
    public function approve(User $user, TimeEntry $timeEntry): bool
    {
        // Managers can approve their team's entries
        // Cannot approve own entries
        return $user->is_manager &&
               $user->id !== $timeEntry->user_id &&
               $user->team_id === $timeEntry->user->team_id;
    }
}

Registering Policies

Policy Registration

<?php
// app/Providers/AuthServiceProvider.php

namespace App\Providers;

use App\Models\Post;
use App\Models\User;
use App\Models\Project;
use App\Models\Comment;
use App\Policies\PostPolicy;
use App\Policies\UserPolicy;
use App\Policies\ProjectPolicy;
use App\Policies\CommentPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array<class-string, class-string>
     */
    protected $policies = [
        Post::class => PostPolicy::class,
        User::class => UserPolicy::class,
        Project::class => ProjectPolicy::class,
        Comment::class => CommentPolicy::class,
        // Laravel will auto-discover policies that follow naming conventions
    ];

    public function boot(): void
    {
        $this->registerPolicies();

        // Global "before" gate - runs before all other checks
        Gate::before(function ($user, $ability) {
            // Super admins can do anything
            if ($user->is_super_admin ?? false) {
                return true;
            }
        });

        // Global "after" gate - runs after all other checks
        Gate::after(function ($user, $ability, $result, $arguments) {
            // Log all authorization attempts for audit
            if (app()->environment('production')) {
                \Log::channel('auth')->info('Authorization attempt', [
                    'user_id' => $user->id,
                    'ability' => $ability,
                    'result' => $result,
                    'arguments' => $arguments,
                ]);
            }
        });

        // Define additional gates
        Gate::define('access-debug', function ($user) {
            return app()->environment('local') && $user->is_developer;
        });

        Gate::define('view-horizon', function ($user) {
            return $user->is_admin && app()->environment('production');
        });

        Gate::define('view-telescope', function ($user) {
            return $user->is_developer;
        });
    }
}

Testing Authorization

Authorization Test Examples

<?php
// tests/Feature/PostPolicyTest.php

namespace Tests\Feature;

use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostPolicyTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_view_their_own_posts()
    {
        $user = User::factory()->create();
        $post = Post::factory()->create(['user_id' => $user->id]);

        $this->assertTrue($user->can('view', $post));
    }

    public function test_user_cannot_view_other_users_unpublished_posts()
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();
        $post = Post::factory()->create([
            'user_id' => $user1->id,
            'is_published' => false
        ]);

        $this->assertFalse($user2->can('view', $post));
    }

    public function test_user_can_view_published_posts()
    {
        $user = User::factory()->create();
        $post = Post::factory()->create([
            'user_id' => User::factory()->create()->id,
            'is_published' => true
        ]);

        $this->assertTrue($user->can('view', $post));
    }

    public function test_admin_can_view_any_post()
    {
        $admin = User::factory()->create(['is_admin' => true]);
        $post = Post::factory()->create([
            'user_id' => User::factory()->create()->id,
            'is_published' => false
        ]);

        $this->assertTrue($admin->can('view', $post));
    }

    public function test_user_can_update_their_own_posts()
    {
        $user = User::factory()->create();
        $post = Post::factory()->create(['user_id' => $user->id]);

        $this->assertTrue($user->can('update', $post));
    }

    public function test_user_cannot_update_other_users_posts()
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();
        $post = Post::factory()->create(['user_id' => $user1->id]);

        $this->assertFalse($user2->can('update', $post));
    }

    public function test_admin_can_update_any_post()
    {
        $admin = User::factory()->create(['is_admin' => true]);
        $post = Post::factory()->create([
            'user_id' => User::factory()->create()->id
        ]);

        $this->assertTrue($admin->can('update', $post));
    }
}

// tests/Feature/PostControllerTest.php

class PostControllerTest extends TestCase
{
    use RefreshDatabase;

    public function test_guest_cannot_access_create_post_page()
    {
        $response = $this->get(route('posts.create'));
        $response->assertRedirect('/login');
    }

    public function test_user_can_access_create_post_page()
    {
        $user = User::factory()->create();
        
        $response = $this->actingAs($user)
                        ->get(route('posts.create'));
        
        $response->assertOk();
    }

    public function test_user_cannot_update_other_users_post()
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();
        $post = Post::factory()->create(['user_id' => $user1->id]);

        $response = $this->actingAs($user2)
                        ->put(route('posts.update', $post), [
                            'title' => 'Updated Title'
                        ]);

        $response->assertForbidden();
    }

    public function test_admin_can_delete_any_post()
    {
        $admin = User::factory()->create(['is_admin' => true]);
        $post = Post::factory()->create([
            'user_id' => User::factory()->create()->id
        ]);

        $response = $this->actingAs($admin)
                        ->delete(route('posts.destroy', $post));

        $response->assertRedirect(route('posts.index'));
        $this->assertSoftDeleted($post);
    }
}

Common Interview Questions & Answers

1. What's the difference between Gates and Policies?

Gates are closure-based and best for actions not tied to specific models (like "view-admin-dashboard"). Policies are class-based and organize authorization logic around specific models (like PostPolicy for Post model actions).

2. When should you use Gates vs Policies?

Use Gates for general abilities that don't relate to specific models. Use Policies when you have model-specific authorization logic, especially for CRUD operations.

3. How does policy auto-discovery work?

Laravel automatically discovers policies if they follow naming conventions (Post model → PostPolicy) and are in the default namespace. You can also manually register policies in AuthServiceProvider.

4. What are Gate::before and Gate::after?

Gate::before runs before all other gate checks - useful for super admin privileges. Gate::after runs after all checks - useful for logging or final overrides.

5. How do you test authorization?

Use Laravel's testing helpers like $user->can('ability'), $this->authorize(), and test for expected HTTP status codes (403 for forbidden, 200 for successful authorization).

6. Can you use both Gates and Policies together?

Yes, they work together seamlessly. Policies are actually implemented using Gates under the hood. You can mix and match based on what makes sense for your application.

Best Practices

  • Keep policies focused on single responsibility
  • Use descriptive names for gates and policy methods
  • Test authorization thoroughly - it's critical for security
  • Use type hints for better IDE support and clarity
  • Follow Laravel conventions for policy method names
  • Use resource controllers with policies for clean CRUD authorization
  • Log sensitive authorization decisions for audit trails
  • Keep business logic out of policies - policies should only handle authorization

Performance Considerations

// Eager load relationships to avoid N+1 in policies
public function view(User $user, Post $post): bool
{
    // Bad - causes N+1 if checking multiple posts
    // return $post->user_id === $user->id;
    
    // Good - if posts are loaded with user relationship
    return $post->user->id === $user->id;
}

// Use caching for expensive authorization checks
public function canAccessPremiumFeature(User $user): bool
{
    return Cache::remember(
        "user_{$user->id}_premium_access",
        3600, // 1 hour
        fn() => $this->checkPremiumAccess($user)
    );
}

You've now mastered Laravel authorization with Gates and Policies! In our next post, we'll explore Laravel CSRF Protection: What It Is and How Laravel Handles It For You to understand web application security.