Laravel Middleware Deep Dive: Authentication, Rate Limiting and Custom Middleware

Master Laravel middleware with practical examples covering authentication guards, rate limiting with throttle, custom middleware creation, and advanced patterns for production applications.

Laravel middleware architecture showing request pipeline with authentication and rate limiting layers

Laravel middleware acts as a filtering layer between incoming HTTP requests and the application logic. Every request passes through a pipeline of middleware classes before reaching a controller, and every response travels back through the same pipeline. Understanding this mechanism is essential for building secure, performant Laravel applications in 2026.

Middleware in a Nutshell

Middleware intercepts HTTP requests before they reach routes. Laravel 12 registers all middleware in bootstrap/app.php using a fluent API. Built-in middleware handles authentication, CSRF protection, session management, and rate limiting out of the box.

How the Laravel Middleware Pipeline Works

The Laravel HTTP kernel processes every request through a stack of middleware. Each middleware receives the request, performs its logic, and either passes the request to the next layer via $next($request) or short-circuits the pipeline by returning a response directly.

This architecture follows the Chain of Responsibility pattern. Middleware can act before the request reaches the controller (e.g., authentication checks), after the response is generated (e.g., adding headers), or both.

app/Http/Middleware/LogRequestTime.phpphp
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

class LogRequestTime
{
    public function handle(Request $request, Closure $next): Response
    {
        $start = microtime(true);          // Capture start time

        $response = $next($request);       // Pass to next middleware

        $duration = microtime(true) - $start;
        Log::info('Request completed', [
            'url'      => $request->url(),
            'method'   => $request->method(),
            'duration' => round($duration * 1000, 2) . 'ms',
        ]);

        return $response;                  // Return response up the stack
    }
}

This middleware wraps the request: it records the start time before processing and logs the duration after the response comes back. This before/after pattern is central to how middleware operates.

Authentication Middleware: Protecting Routes

Laravel ships with the auth middleware alias, mapped to Illuminate\Auth\Middleware\Authenticate. Applying it to a route ensures only authenticated users can access it. Unauthenticated users receive a 401 response (API) or get redirected to the login page (web).

routes/web.phpphp
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\ProfileController;

// Single route protection
Route::get('/dashboard', [DashboardController::class, 'index'])
    ->middleware('auth');

// Group protection for multiple routes
Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'show']);
    Route::put('/profile', [ProfileController::class, 'update']);
    Route::delete('/profile', [ProfileController::class, 'destroy']);
});

Multi-Guard Authentication

Applications with multiple user types (admin panel, customer area, API) benefit from guard-based authentication. The auth middleware accepts a guard parameter to specify which authentication driver to use.

routes/api.phpphp
// API routes use the 'sanctum' guard
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', fn (Request $request) => $request->user());
    Route::apiResource('/orders', OrderController::class);
});

// routes/web.php
// Admin routes use a custom 'admin' guard
Route::middleware('auth:admin')->prefix('admin')->group(function () {
    Route::get('/dashboard', [AdminController::class, 'index']);
    Route::get('/users', [AdminController::class, 'users']);
});

The guard parameter after the colon tells Laravel which authentication configuration to verify against. This keeps authentication logic clean and separated across different parts of the application.

Guest Middleware

The guest middleware is the inverse of auth — it only allows unauthenticated users through. Applying it to login and registration routes prevents already-authenticated users from accessing those pages.

Rate Limiting with Throttle Middleware

Laravel middleware rate limiting protects routes from abuse using the built-in throttle middleware. The simplest form accepts two parameters: the maximum number of requests and the time window in minutes.

routes/api.phpphp
// Allow 60 requests per minute per user
Route::middleware('throttle:60,1')->group(function () {
    Route::get('/posts', [PostController::class, 'index']);
    Route::get('/posts/{post}', [PostController::class, 'show']);
});

// Stricter limit for write operations
Route::middleware(['auth:sanctum', 'throttle:10,1'])->group(function () {
    Route::post('/posts', [PostController::class, 'store']);
    Route::put('/posts/{post}', [PostController::class, 'update']);
});

Named Rate Limiters for Advanced Control

Defining named rate limiters in AppServiceProvider gives fine-grained control over limits based on user context. This approach is more flexible than inline throttle parameters.

app/Providers/AppServiceProvider.phpphp
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Http\Request;

public function boot(): void
{
    // API rate limiter with tiered access
    RateLimiter::for('api', function (Request $request) {
        $user = $request->user();

        if ($user?->hasSubscription('enterprise')) {
            return Limit::perMinute(500)->by($user->id);   // Enterprise: 500/min
        }

        if ($user) {
            return Limit::perMinute(100)->by($user->id);   // Authenticated: 100/min
        }

        return Limit::perMinute(20)->by($request->ip());   // Anonymous: 20/min
    });

    // Login limiter to prevent brute force
    RateLimiter::for('login', function (Request $request) {
        return Limit::perMinute(5)
            ->by($request->ip())                            // Key by IP address
            ->response(function () {                        // Custom exceeded response
                return response()->json([
                    'message' => 'Too many login attempts. Try again in a minute.',
                ], 429);
            });
    });
}

Applying named limiters to routes uses the throttle:name syntax:

routes/api.phpphp
Route::middleware('throttle:api')->group(function () {
    Route::apiResource('/posts', PostController::class);
});

// routes/web.php
Route::middleware('throttle:login')
    ->post('/login', [AuthController::class, 'login']);

The tiered rate limiter above demonstrates a production pattern: enterprise users get higher limits, authenticated users get moderate limits, and anonymous requests are throttled aggressively. The by() method determines the rate limit key — using user ID for authenticated users and IP address as a fallback.

Ready to ace your Laravel interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Building Custom Middleware from Scratch

Creating custom middleware covers scenarios that built-in middleware does not handle. The make:middleware Artisan command scaffolds a new class with the correct structure.

bash
php artisan make:middleware EnsureUserHasRole

Role-Based Access Control Middleware

A common custom middleware pattern enforces role-based authorization at the route level, accepting role names as parameters.

app/Http/Middleware/EnsureUserHasRole.phpphp
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureUserHasRole
{
    public function handle(Request $request, Closure $next, string ...$roles): Response
    {
        $user = $request->user();

        if (! $user || ! $user->hasAnyRole($roles)) {
            abort(403, 'Insufficient permissions.');
        }

        return $next($request);
    }
}

The variadic ...$roles parameter allows passing multiple roles separated by commas. Registration and usage look like this:

bootstrap/app.phpphp
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'role' => \App\Http\Middleware\EnsureUserHasRole::class,
    ]);
})

// routes/web.php
Route::middleware('role:admin')->group(function () {
    Route::get('/admin', [AdminController::class, 'index']);
});

// Multiple roles: admin OR editor can access
Route::middleware('role:admin,editor')->group(function () {
    Route::resource('/articles', ArticleController::class);
});

Request Transformation Middleware

Middleware can modify the request before it reaches the controller. A JSON API middleware that enforces content type headers and trims string inputs:

app/Http/Middleware/ApiRequestSanitizer.phpphp
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class ApiRequestSanitizer
{
    public function handle(Request $request, Closure $next): Response
    {
        // Reject non-JSON requests on API routes
        if (! $request->expectsJson() && $request->isMethod('POST')) {
            return response()->json(
                ['error' => 'Content-Type must be application/json'],
                415
            );
        }

        // Trim all string inputs
        $input = $request->all();
        array_walk_recursive($input, function (&$value) {
            if (is_string($value)) {
                $value = trim($value);
            }
        });
        $request->merge($input);

        return $next($request);
    }
}

This middleware handles two concerns: it validates the content type for POST requests and sanitizes all string inputs by trimming whitespace.

Middleware Registration in Laravel 12

Laravel 12 centralizes all middleware registration in bootstrap/app.php. This replaced the older app/Http/Kernel.php approach that existed before Laravel 11.

bootstrap/app.phpphp
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware) {
        // Global middleware (runs on every request)
        $middleware->append(
            \App\Http\Middleware\LogRequestTime::class
        );

        // Add to the 'web' middleware group
        $middleware->web(append: [
            \App\Http\Middleware\TrackPageViews::class,
        ]);

        // Add to the 'api' middleware group
        $middleware->api(prepend: [
            \App\Http\Middleware\ApiRequestSanitizer::class,
        ]);

        // Register aliases for route-level use
        $middleware->alias([
            'role'       => \App\Http\Middleware\EnsureUserHasRole::class,
            'subscribed' => \App\Http\Middleware\EnsureUserIsSubscribed::class,
        ]);

        // Control execution order
        $middleware->priority([
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\Auth\Middleware\Authenticate::class,
            \App\Http\Middleware\EnsureUserHasRole::class,
        ]);
    })
    ->create();

The priority array matters when multiple middleware are assigned to the same route. Laravel sorts them according to this list, ensuring session is started before authentication runs, and authentication completes before role checks.

Middleware Execution Order

Middleware runs in the order registered. For route-level middleware, the priority array overrides the default order. Always place authentication before authorization middleware to avoid checking roles on unauthenticated requests.

Terminable Middleware for Post-Response Tasks

Terminable middleware executes logic after the response has been sent to the client. This is useful for logging, analytics, or cleanup tasks that should not block the user.

app/Http/Middleware/CollectAnalytics.phpphp
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;

class CollectAnalytics
{
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);  // Pass through without delay
    }

    public function terminate(Request $request, Response $response): void
    {
        // Runs after response is sent to client
        DB::table('analytics')->insert([
            'path'        => $request->path(),
            'method'      => $request->method(),
            'status_code' => $response->getStatusCode(),
            'user_id'     => $request->user()?->id,
            'ip'          => $request->ip(),
            'created_at'  => now(),
        ]);
    }
}

The terminate method receives both the original request and the final response. Register the middleware as a singleton in AppServiceProvider to ensure the same instance handles both handle() and terminate().

Practical Middleware Patterns for Production

Several middleware patterns appear consistently in production Laravel applications.

Maintenance mode bypass — allow internal IPs to access the application during maintenance:

app/Http/Middleware/MaintenanceBypass.phpphp
class MaintenanceBypass
{
    private array $allowedIps = ['192.168.1.0/24', '10.0.0.1'];

    public function handle(Request $request, Closure $next): Response
    {
        if (app()->isDownForMaintenance()) {
            foreach ($this->allowedIps as $ip) {
                if ($request->ip() === $ip) {
                    return $next($request);
                }
            }
        }

        return $next($request);
    }
}

Security headers — add HSTS, content security policy, and other headers to every response:

app/Http/Middleware/SecurityHeaders.phpphp
class SecurityHeaders
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        $response->headers->set('X-Content-Type-Options', 'nosniff');
        $response->headers->set('X-Frame-Options', 'SAMEORIGIN');
        $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
        $response->headers->set(
            'Strict-Transport-Security',
            'max-age=31536000; includeSubDomains'
        );

        return $response;
    }
}

These patterns demonstrate the two primary middleware positions: before the request (maintenance bypass checks the IP and potentially blocks) and after the response (security headers modify the outgoing response).

Ready to ace your Laravel interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Conclusion

  • Laravel middleware operates as a pipeline: each class processes the request, acts on it, and passes it forward or short-circuits with a response
  • The auth middleware protects routes with guard-based authentication, supporting multiple user types via auth:guard syntax
  • Rate limiting through throttle middleware and named RateLimiter::for() definitions enables tiered access control based on user context
  • Custom middleware handles cross-cutting concerns like role checks, request sanitization, and security headers without cluttering controllers
  • All middleware registration in Laravel 12 happens in bootstrap/app.php using a fluent API, with priority controlling execution order
  • Terminable middleware runs post-response tasks (analytics, logging) without impacting user-facing latency
  • Middleware parameters via the :param syntax keep route definitions expressive and middleware classes reusable across different contexts

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Tags

#laravel
#middleware
#authentication
#rate-limiting
#php

Share

Related articles