Implementing Middleware for API Security in Laravel
Learn to build custom middleware in Laravel to enforce resource ownership. Secure your API routes by verifying user access before controllers ever execute.
Previously in this course, we explored handling API validation and form requests to ensure incoming data integrity. While validation ensures the shape of your data is correct, it doesn't guarantee the user has the right to touch that data.
In a multi-user project board, it’s not enough to know that a project_id exists in the database. You must ensure the authenticated user actually owns that project. Relying on controllers to perform these checks leads to repetitive, error-prone code. Today, we’ll move this logic into custom middleware, enforcing security at the route level to keep our controllers lean and secure.
Why Middleware for Authorization?
Middleware provides a layer of "pre-flight" checks for your HTTP requests. By the time a request hits your controller, it should already be authenticated and authorized.
If you scatter ownership checks inside your controller methods, you violate the "Don't Repeat Yourself" (DRY) principle. If you decide to change how project ownership is calculated later, you'll have to hunt down every instance in your codebase. Middleware centralizes this logic, making your API more robust and easier to audit.
Creating the Ownership Middleware
We want to ensure that if a route contains a {project} parameter, the authenticated user is the owner of that project. Let's create a piece of middleware called EnsureProjectOwner.
Run the following Artisan command:
Bashphp artisan make:middleware EnsureProjectOwner
This creates app/Http/Middleware/EnsureProjectOwner.php. We will inject the Request object and use it to verify the relationship between the authenticated user and the project model bound to the route.
PHPnamespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; class EnsureProjectOwner { public function handle(Request $request, Closure $next): Response { #6A9955">// Get the project from the route parameters $project = $request->route('project'); #6A9955">// Check if the user is authorized to access this project if ($project && $project->user_id !== $request->user()->id) { return response()->json([ 'message' => 'You do not have permission to access this project.' ], 403); } return $next($request); } }
Registering and Applying the Middleware
To use this, you must register it in bootstrap/app.php (for Laravel 11+) or app/Http/Kernel.php (for older versions). In a modern Laravel application, you add it to your web or API middleware aliases:
PHP#6A9955">// bootstrap/app.php ->withMiddleware(function (Middleware $middleware) { $middleware->alias([ 'ensure.owner' => \App\Http\Middleware\EnsureProjectOwner::class, ]); })
Now, apply it to your routes in routes/api.php:
PHPRoute::middleware(['auth:sanctum', 'ensure.owner'])->group(function () { Route::get('/projects/{project}', [ProjectController::class, 'show']); Route::put('/projects/{project}', [ProjectController::class, 'update']); });
Handling Unauthorized Access
In the example above, we return a 403 Forbidden status code. This is the correct HTTP response for an authenticated user who is trying to access a resource they don't own.
Pro-tip: Never return a 404 Not Found for authorization failures unless you want to hide the existence of a resource entirely for security reasons (e.g., preventing ID enumeration). In most internal APIs, a 403 is much more helpful for debugging.
Hands-on Exercise
- Refactor: Take your existing
ProjectControllerfrom the service-oriented task management lesson. - Remove: Delete any manual
if ($project->user_id !== auth()->id())checks currently inside your controller methods. - Apply: Wrap your project-related routes with the new
ensure.ownermiddleware. - Test: Create two users. Log in as User A and attempt to
GETa project belonging to User B. Ensure you receive a403response.
Common Pitfalls
- Missing Route Parameters: If your middleware expects a route parameter like
{project}but you apply it to a route that doesn't have one,$request->route('project')will returnnull. Always check if the model exists before comparing IDs. - Order of Middleware: Middleware runs in the order defined in your route group. Always place
auth:sanctumbeforeensure.owner. If you try to check ownership before the user is authenticated,$request->user()will be null, and your app will throw an exception. - Over-complicating Logic: Keep your middleware simple. If the authorization logic requires complex database queries or external service calls, consider using Laravel Policies instead. Middleware is best for simple, route-bound ownership checks.
Recap
We’ve successfully moved our authorization logic out of the controller and into a dedicated middleware layer. This keeps our controllers focused on handling requests and returning responses, rather than policing data access. We’ve enforced security by verifying project ownership, ensuring that users only interact with data that belongs to them.
Up next: We will discuss Database Transactions for Data Integrity, ensuring that our multi-step operations remain atomic and consistent even when things go wrong.
Work with me

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.

FilamentPHP Admin Panel & Dashboard Development
A powerful admin panel for your Laravel app — built with FilamentPHP so you can manage everything without touching the database.