Desarrollo Web 5-8 minutos

Autorización en Laravel 13: Gates y Policies, de cero a producción

Diego Cortés
Diego Cortés
Full Stack Developer & SEO Specialist
Compartir:
Autorización en Laravel 13: Gates y Policies, de cero a producción

La autenticación te dice quién eres; la autorización decide qué puedes hacer. En Laravel 13, las Gates y las Policies centralizan esos permisos en el framework y acaban con los ifs de user_id esparcidos por los controllers.

Autenticación frente a autorización: dos capas distintas

Qué resuelve cada capa y por qué se confunden

La autenticación responde a la pregunta "¿quién eres?": valida las credenciales del usuario y abre una sesión o emite un token. La autorización responde a otra pregunta, "¿qué puedes hacer?", y se evalúa después, sobre una identidad ya confirmada. Confundirlas es el origen de la mayoría de los fallos de permisos: puedes tener login perfecto y, aun así, dejar que cualquier usuario borre los posts de los demás. En Laravel 13 ambos conceptos viven en capas separadas y complementarias, igual que en las apps que ya has construido con Sanctum para tokens o con passkeys para el login sin contraseña.

El modelo de Laravel 13: Gates y Policies, como rutas y controllers

El framework ofrece dos herramientas para la autorización, y la documentación oficial las compara con dos piezas que ya conoces: las Gates son como las rutas y las Policies como los controllers. Las Gates definen permisos con closures simples, ideales para comprobaciones puntuales o globales; las Policies son clases que agrupan la lógica de permisos alrededor de un modelo concreto, como Post, Comment o User. Ambas conviven y se apoyan en los mismos mecanismos de comprobación, así que elegir una no te cierra la puerta de la otra.

Gates: permisos rápidos con closures

Definir un Gate con Gate::define en AppServiceProvider

Una Gate se registra con Gate::define, normalmente en el método boot del AppServiceProvider. Recibe un nombre de ability y un closure que recibe al usuario autenticado y, opcionalmente, los argumentos que necesite para decidir:

use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::define('edit-post', function (User $user, Post $post) {
        return $user->id === $post->user_id;
    });
}

Ese closure debe devolver true o false. Si no te interesa el usuario autenticado (por ejemplo, un permiso global de acceso a un panel), puedes omitirlo como primer parámetro y Laravel lo inyecta igualmente cuando está disponible.

Comprobar permisos: allows(), denies() y authorize()

Con la Gate registrada tienes tres formas de comprobarla. Gate::allows() y Gate::denies() devuelven un booleano sin lanzar excepciones, perfectas para condicionales; Gate::authorize() lanza una AuthorizationException que Laravel convierte en una respuesta 403 si falla:

use Illuminate\Support\Facades\Gate;

if (Gate::allows('edit-post', $post)) {
    // mostrar el botón de editar
}

Gate::authorize('edit-post', $post); // 403 si no tiene permiso

if ($user->can('edit-post', $post)) {
    // el método can() del modelo User es el mismo mecanismo
}

El método can() del modelo User es un atajo que delega en el mismo Gate, así que puedes usarlo en cualquier punto donde tengas el usuario, incluidos los modelos y los servicios.

Gate::before y Gate::after para permisos globales (admin)

Cuando necesitas que un rol tenga acceso a todo sin definir cada ability, Gate::before se evalúa antes que cualquier otra comprobación. Debe devolver true, false o null: si devuelves null, Laravel continúa con la Gate o Policy correspondiente:

Gate::before(function (User $user, string $ability) {
    return $user->isAdmin() ? true : null;
});

Gate::after funciona al revés: se ejecuta después de la comprobación principal y puede sobreescribir el resultado. Es el mecanismo canónico para el patrón "el administrador lo puede todo" sin tocar cada closure.

Policies: autorización por modelo

Crear una policy con php artisan make:policy

Las Policies se generan con artisan. El flag --model crea la clase con los métodos estándar ya esbozados y la vincula por convención:

php artisan make:policy PostPolicy --model=Post

El comando crea app/Policies/PostPolicy.php con métodos como viewAny, view, create, update, delete, restore y forceDelete. Por convención, el primer argumento de cada método es el usuario autenticado y el segundo es la instancia del modelo; en create no hay modelo, así que solo recibe el usuario.

Auto-descubrimiento por convención y registro explícito

Laravel auto-descubre las Policies mientras el modelo viva en App\Models y la policy en App\Policies con el nombre del modelo más el sufijo Policy. Si tus modelos viven en otro namespace, registra la asociación a mano en el AppServiceProvider con Gate::policy(Post::class, PostPolicy::class). El auto-descubrimiento funciona también en Laravel 13, así que en la mayoría de los proyectos no necesitas ni una línea de registro.

Los métodos estándar: viewAny, view, create, update, delete

Cada método decide sobre una acción del recurso. Un ejemplo realista para un blog:

class PostPolicy
{
    public function viewAny(?User $user): bool
    {
        return true; // la lista de posts es pública
    }

    public function view(?User $user, Post $post): bool
    {
        return true; // leer un post no requiere permiso
    }

    public function create(User $user): bool
    {
        return $user->hasVerifiedEmail();
    }

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

    public function delete(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }
}

Fíjate en el detalle del tipo ?User en los métodos públicos: los visitantes sin sesión pueden leer contenido, pero cualquier acción que requiera identidad recibe un User no nullable y Laravel deniega automáticamente a los invitados.

Proteger controllers y rutas

$this->authorize() y authorizeResource() para todo el CRUD

Dentro de un controller que use el trait AuthorizesRequests (el que traen los controllers base de Laravel), $this->authorize() comprueba la ability correspondiente y lanza el 403 si falla. Para proteger un CRUD completo de una sola vez, authorizeResource() mapea cada método del controller con su ability de la policy:

use App\Models\Post;
use App\Policies\PostPolicy;

class PostController extends Controller
{
    public function __construct()
    {
        $this->authorizeResource(Post::class, 'post');
    }

    public function update(Request $request, Post $post)
    {
        // authorizeResource ya ejecutó PostPolicy::update
        $post->update($request->validated());
        return redirect()->route('posts.show', $post);
    }
}

El segundo argumento, 'post', es el nombre del parámetro de ruta que contiene el modelo. Si lo llamas distinto en tus rutas, ajústalo para que Laravel resuelva la instancia.

El middleware can:update,post en rutas

También puedes proteger a nivel de ruta con el middleware can. El primer segmento es la ability y el segundo, el parámetro de ruta con el modelo:

Route::put('/posts/{post}', [PostController::class, 'update'])
    ->middleware('can:update,post');

Así la ruta devuelve 403 antes incluso de entrar al controller, lo que mantiene las rutas como documentación legible de los permisos. Es la opción natural cuando quieres proteger rutas sueltas sin un CRUD completo detrás.

Respuestas personalizadas con Gate::inspect y mensajes de error

Cuando el 403 genérico se queda corto, Gate::inspect() devuelve una respuesta con mensaje propio. El patrón típico: comprobar con allows(), y si falla, usar inspect() para conocer el motivo exacto y mostrarlo al usuario en lugar de una página de error fría:

$response = Gate::inspect('update', $post);

if ($response->allowed()) {
    // continuar
} else {
    return back()->with('error', $response->message());
}

Puedes personalizar el mensaje devolviendo una Gate::deny('Tu mensaje') desde el closure o el método de la policy.

Autorización en Blade

@can, @cannot y @canany en las vistas

En las vistas, las directivas Blade evitan repetir la comprobación en cada plantilla. @can y @cannot reciben la ability y el modelo; @canany acepta una lista y muestra el bloque si el usuario puede alguna:

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

@cannot('delete', $post)
    <p>Solo el autor puede borrar este post.</p>
@endcannot

Estas directivas ejecutan exactamente el mismo mecanismo que authorize() y el middleware, así que lo que ves en la vista coincide siempre con lo que valida el servidor.

@auth y @guest: cuándo usarlos (y cuándo no)

@auth y @guest comprueban únicamente si hay sesión, no si hay permiso. Son útiles para decidir entre "Iniciar sesión" y "Mi panel", pero nunca deben sustituir a @can para proteger acciones concretas: un usuario logueado sin permisos seguiría viendo el botón. La regla práctica: sesión para la identidad, @can para la autorización.

Ejemplo completo: un blog con autores, editores y admin

Policies de Post y Comment con el autor como dueño

Unamos todo en el ejemplo conductor: un blog en Laravel 13 donde cada autor edita y borra solo sus posts. La policy de Post ya la tienes arriba; la de Comment añade la moderación, de modo que un autor puede borrar los comentarios de sus propios posts aunque no los haya escrito él:

class CommentPolicy
{
    public function delete(User $user, Comment $comment): bool
    {
        // el autor del comentario o el autor del post pueden borrarlo
        return $user->id === $comment->user_id
            || $user->id === $comment->post->user_id;
    }
}

Este es el punto donde se nota la diferencia frente al enfoque de los ifs sueltos: la regla vive en un único sitio y se aplica igual desde el controller, la ruta y la vista.

El Gate esAdmin con Gate::before para el rol administrador

El administrador lo puede todo sin duplicar condiciones en cada policy. Con el Gate::before visto antes, cualquier ability devuelve true para él y el resto de reglas se saltan; los demás usuarios siguen evaluando sus policies normalmente:

Gate::before(function (User $user, string $ability) {
    return $user->hasRole('admin') ? true : null;
});

Proteger el panel y las rutas de moderación

El panel de administración se protege con una Gate global, no con una policy, porque no depende de un modelo: Gate::define('access-dashboard', fn (User $user) => $user->hasAnyRole(['admin', 'editor'])) y luego el middleware en el grupo de rutas del panel. Las rutas de moderación usan can:delete,comment para que solo quienes cumplan la policy de Comment puedan borrar.

Buenas prácticas y errores comunes

No meter lógica de negocio en los closures

Las Gates deben delegar en el modelo o en un servicio, no contener lógica de negocio. Si el closure necesita comprobar suscripciones, estados o fechas, esa lógica pertenece al modelo o a un servicio y el closure solo la consulta. Así las reglas son testables por separado y no se duplican entre Gates, Policies y validadores.

Tests de autorización con actingAs y Pest

La autorización se testea con actingAs(), que autentica a un usuario para la petición. Con Pest y los asserts de respuesta, el test queda corto y legible:

it('deniega editar el post de otro usuario', function () {
    $author = User::factory()->create();
    $other  = User::factory()->create();
    $post   = Post::factory()->for($author)->create();

    $this->actingAs($other)
        ->put("/posts/{$post->id}", ['title' => 'Hack'])
        ->assertForbidden();
});

it('permite al admin editar cualquier post', function () {
    $admin = User::factory()->admin()->create();
    $post  = Post::factory()->create();

    $this->actingAs($admin)
        ->put("/posts/{$post->id}", ['title' => 'Editado'])
        ->assertOk();
});

Si vienes del post de Pest 3 del blog, verás que encaja con el mismo estilo: describe el comportamiento, no la implementación.

Conclusión

La autorización en Laravel 13 se resuelve con dos herramientas que se complementan: Gates para permisos puntuales y globales, Policies para el control por recurso. Empieza definiendo las policies de tus modelos con make:policy, protege los controllers con authorizeResource(), añade el Gate::before del admin y refleja los permisos en Blade con @can. Tu código dejará de preguntarse quién es el usuario para ocuparse de lo que de verdad importa: qué puede hacer. Sigue leyendo el blog para más guías prácticas de Laravel 13.

Categorías