Middleware vs Gates/Policies

Learn when to use Middleware for authentication vs Gates/Policies for authorization

Middleware and Gates/Policies both control access, but they serve different purposes.

Middleware - HTTP Request Filtering
• Runs BEFORE the controller
• Checks if a user CAN MAKE this type of request
• Examples: Is user logged in? Is email verified? Is request rate-limited?
• Answers: "Is this user allowed to access this route at all?"

Gates/Policies - Business Logic Authorization
• Runs INSIDE your controller logic
• Checks if a user CAN PERFORM an action on a SPECIFIC resource
• Examples: Can this user edit THIS post? Can this user delete THIS comment?
• Answers: "Is this user allowed to perform this action on this specific item?"

Quick Decision Guide:
Use Middleware when: Checking user status (logged in, email verified, subscription active)
Use Gates/Policies when: Checking permissions on specific models (can edit this post, can delete this comment)

Real-World Example:
Blog System:
• Middleware: Ensure user is authenticated (auth middleware)
• Policy: Check if THIS user can edit THIS specific blog post (they must be the author or admin)

Code Examples

Middleware - Request-Level Control
✓ Best Practice php
<?php
// Middleware checks if user CAN ACCESS the route
// app/Http/Middleware/EnsureUserIsSubscribed.php
class EnsureUserIsSubscribed
{
    public function handle(Request $request, Closure $next)
    {
        // Check user's subscription status (not a specific resource)
        if (!$request->user() || !$request->user()->isSubscribed()) {
            return redirect('subscribe')
                ->with('error', 'You need an active subscription');
        }
        
        // User is subscribed, continue to controller
        return $next($request);
    }
}

// Usage in routes:
Route::middleware(['auth', 'subscribed'])->group(function () {
    Route::get('/premium/courses', [CourseController::class, 'index']);
    Route::get('/premium/videos', [VideoController::class, 'index']);
    Route::get('/premium/downloads', [DownloadController::class, 'index']);
});

// Middleware runs BEFORE any controller
// It doesn't care about WHICH course or video
// Only cares about: "Is user subscribed?"

// Use Cases for Middleware:
// ✅ Authentication (is user logged in?)
// ✅ Email verification (is email verified?)
// ✅ Subscription status (is subscription active?)
// ✅ Rate limiting (has user exceeded API limits?)
// ✅ Role checking (is user an admin?) - broad check

💡 Middleware runs before the controller and checks request-level access. It doesn't know about specific resources, only about the user's general status.

Policies - Resource-Level Control
✓ Best Practice php
<?php
// Policy checks if user CAN PERFORM ACTION on SPECIFIC resource
// app/Policies/PostPolicy.php
class PostPolicy
{
    // Can this user update THIS specific post?
    public function update(User $user, Post $post): bool
    {
        // Only the author or admins can update
        return $user->id === $post->user_id 
            || $user->isAdmin();
    }
    
    // Can this user delete THIS specific post?
    public function delete(User $user, Post $post): bool
    {
        // Only admins can delete
        return $user->isAdmin();
    }
    
    // Can this user publish THIS specific post?
    public function publish(User $user, Post $post): bool
    {
        // Only the author can publish their own draft
        return $user->id === $post->user_id 
            && $post->status === 'draft';
    }
}

// Usage in controller:
class PostController extends Controller
{
    public function update(Request $request, Post $post)
    {
        // Check authorization for THIS specific post
        $this->authorize('update', $post);
        
        // If we reach here, user is authorized
        $post->update($request->validated());
        
        return redirect()->route('posts.show', $post);
    }
}

// Policy runs INSIDE the controller
// It knows about the SPECIFIC post being updated
// Answers: "Can THIS user update THIS post?"

// Use Cases for Policies:
// ✅ Editing specific models (can edit THIS post?)
// ✅ Deleting specific resources (can delete THIS comment?)
// ✅ Viewing private content (can view THIS draft?)
// ✅ Resource ownership (is THIS user the owner?)

💡 Policies check permissions on specific model instances. They run inside your controller and have access to both the user and the specific resource being accessed.

Gates - Simple Authorization Checks
💡 Solution php
<?php
// Gates are for simple, non-model authorization
// app/Providers/AuthServiceProvider.php
class AuthServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Define a gate for admin access
        Gate::define('access-admin-panel', function (User $user) {
            return $user->role === 'admin';
        });
        
        // Define a gate for feature flags
        Gate::define('use-beta-features', function (User $user) {
            return $user->beta_tester || $user->isAdmin();
        });
        
        // Define a gate for time-based access
        Gate::define('post-after-hours', function (User $user) {
            $hour = now()->hour;
            return $user->isAdmin() || ($hour >= 9 && $hour <= 17);
        });
    }
}

// Usage in controllers:
class AdminController extends Controller
{
    public function index()
    {
        // Simple check not tied to a specific model
        if (Gate::denies('access-admin-panel')) {
            abort(403, 'Access denied');
        }
        
        return view('admin.dashboard');
    }
}

// Usage in Blade views:
@can('use-beta-features')
    <a href="/beta/new-editor">Try Beta Editor</a>
@endcan

// Gates vs Policies:
// Gates:     Simple, not tied to models
// Policies:  Complex, tied to specific model instances

💡 Gates are for simple authorization that isn't tied to a specific model. Use them for general permissions like "access admin panel" or "use beta features".

Combining Middleware + Policies (Real Example)
💡 Solution php
<?php
// Real-world blog platform example

// Step 1: Route with middleware (request-level check)
Route::middleware(['auth', 'verified'])->group(function () {
    Route::resource('posts', PostController::class);
});
// Middleware asks: "Is user logged in and verified?"

// Step 2: Controller with policy (resource-level check)
class PostController extends Controller
{
    public function edit(Post $post)
    {
        // Policy asks: "Can THIS user edit THIS specific post?"
        $this->authorize('update', $post);
        
        return view('posts.edit', compact('post'));
    }
    
    public function update(Request $request, Post $post)
    {
        $this->authorize('update', $post);
        
        $post->update($request->validated());
        
        return redirect()->route('posts.show', $post);
    }
    
    public function destroy(Post $post)
    {
        $this->authorize('delete', $post);
        
        $post->delete();
        
        return redirect()->route('posts.index');
    }
}

// Security layers:
// Layer 1 (Middleware): Is user authenticated and verified?
// Layer 2 (Policy):     Can THIS user modify THIS post?

// This is the Laravel way:
// ✅ Middleware for broad access control
// ✅ Policies for specific resource permissions
// ✅ Together they create a secure system

// Timeline of a request to edit a post:
// 1. Middleware checks if user is logged in → ✓
// 2. Middleware checks if email is verified → ✓
// 3. Controller loads the specific post → ✓
// 4. Policy checks if THIS user can edit THIS post → ✓
// 5. Edit view is shown

💡 In real applications, you combine middleware (broad access) with policies (specific permissions). Middleware runs first to filter requests, then policies check specific resource permissions.