Advanced Laravel

Authorization: Policies & Gates Deep Dive

18 min Lesson 8 of 40

Authorization: Policies & Gates Deep Dive

Master Laravel's authorization system with advanced policy patterns, gate definitions, before/after callbacks, resource authorization, and authorization for guest users.

Understanding Gates vs Policies

Gates and policies are Laravel's two authorization mechanisms:

// Gates: Simple closures for quick authorization checks Gate::define('update-post', function (User $user, Post $post) { return $user->id === $post->user_id; }); // Policies: Classes that organize authorization logic around models class PostPolicy { public function update(User $user, Post $post) { return $user->id === $post->user_id; } }
Pro Tip: Use gates for simple, model-independent checks. Use policies for model-specific authorization that groups related logic together.

Advanced Policy Patterns

Create comprehensive policies with advanced patterns:

// Generate policy php artisan make:policy PostPolicy --model=Post // app/Policies/PostPolicy.php namespace App\Policies; use App\Models\User; use App\Models\Post; class PostPolicy { // Executed before all other methods public function before(User $user, string $ability) { // Super admins can do anything if ($user->isSuperAdmin()) { return true; } // Return null to continue to other methods return null; } // View any posts (index) public function viewAny(User $user) { return $user->hasPermission('view-posts'); } // View single post public function view(?User $user, Post $post) { // Guest users can view published posts if ($post->is_published) { return true; } // Authenticated users can view their own drafts return $user && $user->id === $post->user_id; } // Create new post public function create(User $user) { return $user->hasPermission('create-posts') && !$user->isBanned(); } // Update existing post public function update(User $user, Post $post) { // Owner can update if ($user->id === $post->user_id) { return true; } // Editors can update any post if ($user->hasRole('editor')) { return true; } return false; } // Delete post public function delete(User $user, Post $post) { // Only owner can delete if ($user->id === $post->user_id) { return true; } return false; } // Force delete (permanent) public function forceDelete(User $user, Post $post) { return $user->hasRole('admin'); } // Restore soft-deleted post public function restore(User $user, Post $post) { return $user->hasRole(['admin', 'editor']); } // Custom ability: publish post public function publish(User $user, Post $post) { return $user->hasRole(['admin', 'editor']) || ($user->id === $post->user_id && $user->isVerified()); } }

Registering Policies

Register policies in AuthServiceProvider:

// app/Providers/AuthServiceProvider.php namespace App\Providers; use App\Models\Post; use App\Policies\PostPolicy; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; class AuthServiceProvider extends ServiceProvider { protected $policies = [ Post::class => PostPolicy::class, ]; public function boot() { $this->registerPolicies(); // Auto-discover policies Gate::guessPolicyNamesUsing(function ($modelClass) { return 'App\\Policies\\' . class_basename($modelClass) . 'Policy'; }); } }

Using Authorization in Controllers

Apply authorization checks in various ways:

namespace App\Http\Controllers; class PostController extends Controller { // Method 1: authorize() helper public function update(Request $request, Post $post) { $this->authorize('update', $post); // If we reach here, user is authorized $post->update($request->validated()); return redirect()->route('posts.show', $post); } // Method 2: Gate facade public function destroy(Post $post) { if (Gate::denies('delete', $post)) { abort(403, 'You cannot delete this post.'); } $post->delete(); return redirect()->route('posts.index'); } // Method 3: User can() method public function publish(Post $post) { if (!auth()->user()->can('publish', $post)) { return back()->with('error', 'You cannot publish this post.'); } $post->update(['is_published' => true]); return back()->with('success', 'Post published!'); } // Method 4: Middleware public function __construct() { $this->middleware('can:create,App\Models\Post')->only(['create', 'store']); $this->middleware('can:update,post')->only(['edit', 'update']); $this->middleware('can:delete,post')->only(['destroy']); } // Method 5: authorizeResource (automatic) public function __construct() { $this->authorizeResource(Post::class, 'post'); } }
Note: The authorizeResource() method automatically maps controller methods to policy methods, reducing boilerplate authorization code.

Advanced Gate Definitions

Define complex gates with parameters and logic:

// app/Providers/AuthServiceProvider.php public function boot() { $this->registerPolicies(); // Simple gate Gate::define('view-dashboard', function (User $user) { return $user->hasRole(['admin', 'manager']); }); // Gate with resource Gate::define('assign-role', function (User $user, User $target, string $role) { // Can't assign role to yourself if ($user->id === $target->id) { return false; } // Can only assign roles lower than your own return $user->role_level > Role::where('name', $role)->first()->level; }); // Gate with multiple resources Gate::define('transfer-post', function (User $user, Post $post, User $newOwner) { return $user->id === $post->user_id && $newOwner->hasPermission('own-posts'); }); // Gate that allows guest users Gate::define('read-post', function (?User $user, Post $post) { if ($post->is_public) { return true; } return $user && $user->id === $post->user_id; }); // Intercepting authorization checks Gate::before(function (User $user, string $ability) { // Super admins bypass all checks if ($user->hasRole('super-admin')) { return true; } // Return null to continue normal authorization return null; }); Gate::after(function (User $user, string $ability, bool|null $result) { // Log authorization failures if ($result === false) { Log::info("Authorization denied: {$ability}", [ 'user_id' => $user->id, 'ability' => $ability, ]); } // Return null to preserve original result return null; }); }

Policy Responses with Messages

Return detailed responses from policies:

use Illuminate\Auth\Access\Response; class PostPolicy { public function update(User $user, Post $post) { if ($user->id === $post->user_id) { return Response::allow(); } if ($post->is_locked) { return Response::deny('This post is locked and cannot be edited.'); } if ($user->isBanned()) { return Response::deny('Your account has been banned.', 403); } return Response::deny('You do not have permission to edit this post.'); } public function delete(User $user, Post $post) { if ($post->hasComments()) { return Response::denyWithStatus( 422, 'Cannot delete post with comments. Archive it instead.' ); } return $user->id === $post->user_id ? Response::allow() : Response::deny('Only the author can delete this post.'); } } // Handling in controller public function update(Request $request, Post $post) { $response = Gate::inspect('update', $post); if ($response->denied()) { return back()->with('error', $response->message()); } // Continue with update }
Warning: Always check authorization before performing sensitive operations, even if the UI hides unauthorized actions. Client-side restrictions are not security measures.

Authorization in Blade Templates

Control UI elements based on authorization:

{{-- Check single ability --}} @can('update', $post) <a href="{{ route('posts.edit', $post) }}">Edit</a> @endcan {{-- Check with else --}} @can('delete', $post) <button type="submit">Delete</button> @else <span class="text-muted">Cannot delete</span> @endcan {{-- Check cannot --}} @cannot('create', App\Models\Post::class) <p>You don't have permission to create posts.</p> @endcannot {{-- Check multiple abilities (OR) --}} @canany(['update', 'delete'], $post) <div class="post-actions"> @can('update', $post) <a href="{{ route('posts.edit', $post) }}">Edit</a> @endcan @can('delete', $post) <button>Delete</button> @endcan </div> @endcanany {{-- Check gate without resource --}} @can('view-dashboard') <a href="{{ route('dashboard') }}">Dashboard</a> @endcan {{-- Guest authorization --}} @guest @can('read-post', $post) <p>{{ $post->content }}</p> @endcan @endguest

Authorization for API Resources

Apply authorization in API contexts:

namespace App\Http\Controllers\Api; class PostController extends Controller { public function index(Request $request) { $this->authorize('viewAny', Post::class); return PostResource::collection( Post::paginate() ); } public function store(Request $request) { $this->authorize('create', Post::class); $post = Post::create($request->validated()); return new PostResource($post); } public function update(Request $request, Post $post) { $response = Gate::inspect('update', $post); if ($response->denied()) { return response()->json([ 'message' => $response->message(), ], $response->status() ?? 403); } $post->update($request->validated()); return new PostResource($post); } // Bulk authorization public function bulkDelete(Request $request) { $postIds = $request->input('post_ids'); $posts = Post::findMany($postIds); // Check authorization for each post foreach ($posts as $post) { if (Gate::denies('delete', $post)) { return response()->json([ 'message' => "Cannot delete post {$post->id}", ], 403); } } // All authorized, proceed with deletion Post::destroy($postIds); return response()->json([ 'message' => 'Posts deleted successfully' ]); } }

Dynamic Policy Registration

Register policies programmatically based on application state:

// app/Providers/AuthServiceProvider.php public function boot() { $this->registerPolicies(); // Register policies from database $customPolicies = PolicyConfiguration::all(); foreach ($customPolicies as $config) { Gate::define($config->name, function (User $user) use ($config) { return $user->hasPermission($config->required_permission); }); } // Register policies from plugins $plugins = app(PluginManager::class)->getActivePlugins(); foreach ($plugins as $plugin) { foreach ($plugin->getPolicies() as $modelClass => $policyClass) { Gate::policy($modelClass, $policyClass); } } }

Testing Authorization

Write tests for authorization logic:

// tests/Feature/PostAuthorizationTest.php namespace Tests\Feature; use Tests\TestCase; use App\Models\User; use App\Models\Post; class PostAuthorizationTest extends TestCase { public function test_user_can_update_own_post() { $user = User::factory()->create(); $post = Post::factory()->create(['user_id' => $user->id]); $this->assertTrue($user->can('update', $post)); } public function test_user_cannot_update_others_post() { $user = User::factory()->create(); $post = Post::factory()->create(); $this->assertFalse($user->can('update', $post)); } public function test_admin_can_delete_any_post() { $admin = User::factory()->admin()->create(); $post = Post::factory()->create(); $this->assertTrue($admin->can('delete', $post)); } public function test_unauthorized_update_returns_403() { $user = User::factory()->create(); $post = Post::factory()->create(); $response = $this->actingAs($user) ->put(route('posts.update', $post), [ 'title' => 'New Title' ]); $response->assertForbidden(); } }
Exercise 1: Create a comprehensive authorization system for a blog with the following rules:
1. Users can view all published posts, but only their own drafts
2. Users can edit their own posts within 24 hours of creation
3. Editors can edit any post, but not delete them
4. Admins can do everything
5. Banned users cannot create or edit posts
Implement with policies, test all scenarios, and add Blade directives.
Exercise 2: Build a document sharing system where:
1. Document owners can view, edit, delete, and share
2. Collaborators (editor role) can view and edit
3. Viewers can only view
4. Implement a "transfer ownership" feature with proper authorization
5. Add folder-level permissions that cascade to documents
Use policies with custom abilities and response messages.
Exercise 3: Implement a multi-level approval system:
1. Regular users submit requests
2. Managers can approve/reject up to $10,000
3. Directors can approve/reject up to $50,000
4. Executives can approve any amount
5. Users can view their own requests, managers see team requests
Use gates with multiple parameters and custom before/after hooks to log all authorization checks.