Skip to content

Middleware y autorización ​

Ya sabes quién es el usuario. Ahora toca decidir qué puede hacer. En esta lección crearás tus propios middleware (filtros que se ejecutan antes o después de cada petición) y usarás Gates y Policies para controlar permisos de forma ordenada: que un cliente sólo vea sus proyectos, que sólo un admin pueda borrar, etc.

Concepto ​

  • Middleware: son capas por las que pasa la petición antes de llegar al controlador, como los filtros de seguridad de un aeropuerto. Cada uno puede dejarla pasar, modificarla o detenerla (por ejemplo, redirigir al login).
  • Gate: una regla de permiso suelta, tipo "¿puede ver el panel de administración?".
  • Policy: un conjunto de reglas para un modelo: "¿puede ver/editar/borrar este proyecto?".
txt
Petición → [middleware auth] → [middleware admin] → Controlador → (policy: ¿puede editar este post?) → Respuesta

Middleware ​

Crear un middleware ​

bash
php artisan make:middleware EnsureUserIsAdmin
php
<?php

namespace App\Http\Middleware;

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

class EnsureUserIsAdmin
{
    /**
     * $next($request) deja pasar la petición a la siguiente capa.
     */
    public function handle(Request $request, Closure $next): Response
    {
        // Si no hay usuario o no es admin, cortamos aquí con 403
        if (! $request->user()?->isAdmin()) {
            abort(403, 'Sólo administradores.');
        }

        return $next($request);
    }
}

Registrarlo (Laravel 11/12) ​

En Laravel 11+ ya no existe app/Http/Kernel.php. Todo se configura en bootstrap/app.php:

php
<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        // Alias para usarlo en rutas: ->middleware('admin')
        $middleware->alias([
            'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class,
        ]);

        // Añadir un middleware a TODAS las rutas web
        $middleware->web(append: [
            \App\Http\Middleware\SetLocaleFromUser::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

Uso en rutas:

php
Route::middleware(['auth', 'admin'])
    ->prefix('admin')
    ->name('admin.')
    ->group(function () {
        Route::get('/', [AdminController::class, 'index'])->name('dashboard');
        Route::resource('users', Admin\UserController::class);
    });

// También puedes usar la clase directamente sin alias
Route::get('/reportes', ReportController::class)->middleware(EnsureUserIsAdmin::class);

Middleware con parámetros ​

Un solo middleware para varios roles:

php
class EnsureUserHasRole
{
    // Los parámetros llegan después de $next: ->middleware('role:admin,editor')
    public function handle(Request $request, Closure $next, string ...$roles): Response
    {
        if (! in_array($request->user()?->role, $roles, true)) {
            abort(403);
        }

        return $next($request);
    }
}
php
$middleware->alias(['role' => \App\Http\Middleware\EnsureUserHasRole::class]);

Route::get('/blog/admin', ...)->middleware('role:admin,editor');

Middleware "después" de la respuesta ​

php
public function handle(Request $request, Closure $next): Response
{
    $response = $next($request); // primero deja que el controlador responda

    // Luego modificamos la respuesta: añadir cabeceras de seguridad
    $response->headers->set('X-Frame-Options', 'DENY');
    $response->headers->set('X-Content-Type-Options', 'nosniff');

    return $response;
}

Middleware integrados útiles: auth, guest, verified, throttle:60,1 (límite de peticiones), signed (URLs firmadas), can:update,post (autorización), password.confirm (pide la contraseña de nuevo antes de acciones sensibles).

Gates ​

Para permisos que no dependen de un modelo concreto. Se definen en App\Providers\AppServiceProvider:

php
<?php

namespace App\Providers;

use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // ¿Puede ver el panel de administración?
        Gate::define('view-admin', fn (User $user) => $user->role === 'admin');

        // ¿Puede exportar reportes? (admins y contadores)
        Gate::define('export-reports', fn (User $user) => in_array($user->role, ['admin', 'accountant']));

        // Super admin: pasa todos los permisos
        Gate::before(function (User $user, string $ability) {
            if ($user->email === 'dueño@cliente.com') {
                return true; // true = permitido, null = sigue evaluando normal
            }
        });
    }
}

Uso:

php
use Illuminate\Support\Facades\Gate;

// En un controlador: lanza 403 si no puede
Gate::authorize('export-reports');

// Como booleano
if (Gate::allows('view-admin')) { /* ... */ }
if ($request->user()->can('view-admin')) { /* ... */ }

// En rutas
Route::get('/admin', ...)->middleware('can:view-admin');

Policies ​

Las policies agrupan los permisos de un modelo. Caso típico: cada usuario sólo puede editar y borrar sus propios proyectos.

bash
php artisan make:policy ProjectPolicy --model=Project
php
<?php

namespace App\Policies;

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

class ProjectPolicy
{
    /**
     * Se ejecuta antes que cualquier otro método.
     * Los admins pueden todo.
     */
    public function before(User $user, string $ability): ?bool
    {
        return $user->role === 'admin' ? true : null; // null = decide el método normal
    }

    // Listado: cualquier usuario autenticado
    public function viewAny(User $user): bool
    {
        return true;
    }

    // Ver uno: sólo el dueño
    public function view(User $user, Project $project): bool
    {
        return $project->user_id === $user->id;
    }

    // Crear: usuarios con correo verificado
    public function create(User $user): bool
    {
        return $user->hasVerifiedEmail();
    }

    public function update(User $user, Project $project): bool
    {
        return $project->user_id === $user->id;
    }

    // Borrar: el dueño, y sólo si el proyecto no está cerrado
    public function delete(User $user, Project $project): bool
    {
        return $project->user_id === $user->id && $project->status !== 'closed';
    }
}

Laravel descubre la policy automáticamente si sigue la convención (App\Models\Project → App\Policies\ProjectPolicy). No hay que registrarla.

Usar la policy en el controlador ​

php
<?php

namespace App\Http\Controllers;

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

class ProjectController extends Controller
{
    public function show(Project $project)
    {
        // Llama a ProjectPolicy::view(). Si devuelve false → 403
        Gate::authorize('view', $project);

        return view('projects.show', compact('project'));
    }

    public function create()
    {
        // Métodos sin modelo concreto: se pasa la clase
        Gate::authorize('create', Project::class);

        return view('projects.create');
    }

    public function update(Request $request, Project $project)
    {
        Gate::authorize('update', $project);

        $project->update($request->validate([
            'name' => ['required', 'max:255'],
        ]));

        return back()->with('status', 'Proyecto actualizado.');
    }

    public function destroy(Project $project)
    {
        Gate::authorize('delete', $project);

        $project->delete();

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

Nota de versión

En Laravel 11+ el Controller base viene vacío. Si prefieres $this->authorize('update', $project), añade el trait Illuminate\Foundation\Auth\Access\AuthorizesRequests a app/Http/Controllers/Controller.php. Gate::authorize() funciona siempre sin configurar nada.

También puedes autorizar desde la ruta:

php
Route::put('/projects/{project}', [ProjectController::class, 'update'])
    ->middleware('can:update,project'); // "project" = nombre del parámetro de ruta

O desde un Form Request:

php
public function authorize(): bool
{
    return $this->user()->can('update', $this->route('project'));
}

Usar la policy en Blade ​

Oculta botones que el usuario no puede usar:

blade
@can('update', $project)
    <a href="{{ route('projects.edit', $project) }}">Editar</a>
@endcan

@can('delete', $project)
    <form method="POST" action="{{ route('projects.destroy', $project) }}">
        @csrf
        @method('DELETE')
        <button onclick="return confirm('¿Seguro?')">Eliminar</button>
    </form>
@endcan

@cannot('create', App\Models\Project::class)
    <p>Verifica tu correo para crear proyectos.</p>
@endcannot

Error grave

Ocultar el botón en Blade no es seguridad. Si sólo usas @can en la vista pero no autorizas en el controlador, cualquiera puede enviar la petición a mano (con Postman o la consola del navegador). Autoriza siempre en el servidor; @can es sólo para la interfaz.

Filtrar listados ​

Las policies deciden sobre un registro. Para listados, filtra con la consulta:

php
public function index(Request $request)
{
    $projects = $request->user()->isAdmin()
        ? Project::latest()->paginate(15)                    // admin ve todo
        : $request->user()->projects()->latest()->paginate(15); // el resto, sólo lo suyo

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

Error común

Hacer Project::findOrFail($id) sin comprobar el dueño. Un usuario cambia el número en la URL (/projects/8 → /projects/9) y ve proyectos ajenos. A esta vulnerabilidad se le llama IDOR (referencia directa insegura) y es de las más comunes. Ver OWASP Top 10.

Respuestas con mensaje ​

php
use Illuminate\Auth\Access\Response;

public function delete(User $user, Project $project): Response
{
    if ($project->status === 'closed') {
        return Response::deny('No puedes borrar un proyecto cerrado.');
    }

    return $project->user_id === $user->id
        ? Response::allow()
        : Response::denyAsNotFound(); // responde 404 para no revelar que existe
}

Ejercicios ​

  1. Crea un middleware EnsureAccountIsActive que cierre la sesión y redirija al login con un mensaje si la columna is_active del usuario es false. Regístralo para todas las rutas web.
  2. Crea una CommentPolicy: cualquiera autenticado puede crear; sólo el autor puede editar en los primeros 15 minutos; el autor o un admin pueden borrar.
Ver solución 1
php
<?php

namespace App\Http\Middleware;

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

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

        if ($user && ! $user->is_active) {
            Auth::logout();
            $request->session()->invalidate();
            $request->session()->regenerateToken();

            return redirect()->route('login')
                ->withErrors(['email' => 'Tu cuenta está desactivada. Contacta al administrador.']);
        }

        return $next($request);
    }
}
php
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        \App\Http\Middleware\EnsureAccountIsActive::class,
    ]);
})
Ver solución 2
php
<?php

namespace App\Policies;

use App\Models\Comment;
use App\Models\User;

class CommentPolicy
{
    public function create(User $user): bool
    {
        return true;
    }

    public function update(User $user, Comment $comment): bool
    {
        return $comment->user_id === $user->id
            && $comment->created_at->gt(now()->subMinutes(15));
    }

    public function delete(User $user, Comment $comment): bool
    {
        return $comment->user_id === $user->id || $user->role === 'admin';
    }
}

Resumen ​

  • Los middleware filtran peticiones; en Laravel 11/12 se registran en bootstrap/app.php con withMiddleware().
  • Usa alias ('admin' => ...) y parámetros (role:admin,editor) para reutilizarlos.
  • Gates: permisos sueltos; Policies: permisos por modelo, descubiertas automáticamente.
  • Autoriza con Gate::authorize(), can: en rutas o authorize() en Form Requests.
  • @can en Blade sólo oculta la interfaz: la seguridad real está en el servidor.
  • Filtra listados por dueño para evitar IDOR.

Academia Web — aprende, copia y construye.