Lección 31 de 45 15 min de lectura

Autorización y Gates

Autenticación responde "¿quién eres?", pero autorización responde "¿qué puedes hacer?". Laravel proporciona Gates y Policies para controlar qué acciones puede realizar cada usuario en tu aplicación.

Autenticación vs Autorización

Es importante entender la diferencia:

  • Autenticación: Verifica la identidad del usuario (login)
  • Autorización: Determina qué puede hacer ese usuario

Por ejemplo, en un blog: cualquier usuario autenticado puede crear posts, pero solo el autor de un post puede editarlo o eliminarlo. Eso es autorización.

Gates: La forma más simple

Los Gates son closures que determinan si un usuario puede realizar una acción. Se definen en el archivo app/Providers/AppServiceProvider.php:

php
<?php

declare(strict_types=1);

namespace App\Providers;

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

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Gate simple: ¿puede el usuario editar este post?
        Gate::define('update-post', function (User $user, Post $post): bool {
            return $user->id === $post->user_id;
        });

        // Gate para administradores
        Gate::define('access-admin', function (User $user): bool {
            return $user->is_admin === true;
        });
    }
}

El primer parámetro del closure siempre es el usuario autenticado. Los siguientes son los recursos que necesitas para tomar la decisión.

Usar Gates en controladores

Una vez definido un Gate, puedes verificarlo de varias formas en tus controladores:

php
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\View\View;

class PostController extends Controller
{
    public function edit(Post $post): View
    {
        // Opción 1: Gate::allows() - retorna boolean
        if (! Gate::allows('update-post', $post)) {
            abort(403, 'No tienes permiso para editar este post.');
        }

        return view('posts.edit', compact('post'));
    }

    public function update(Request $request, Post $post): RedirectResponse
    {
        // Opción 2: Gate::authorize() - lanza excepción automáticamente
        Gate::authorize('update-post', $post);

        $post->update($request->validated());

        return redirect()->route('posts.show', $post);
    }

    public function destroy(Post $post): RedirectResponse
    {
        // Opción 3: Desde el Request
        if ($request->user()->cannot('update-post', $post)) {
            abort(403);
        }

        $post->delete();

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

Consejo: Usa Gate::authorize() cuando quieras que Laravel maneje el error 403 automáticamente. Usa Gate::allows() cuando necesites lógica personalizada al denegar.

Métodos can y cannot

El modelo User incluye métodos convenientes para verificar Gates:

php
<?php

// En un controlador
public function edit(Request $request, Post $post): View
{
    // can() retorna true si el usuario puede realizar la acción
    if ($request->user()->can('update-post', $post)) {
        return view('posts.edit', compact('post'));
    }

    abort(403);
}

// cannot() es lo opuesto
if ($request->user()->cannot('update-post', $post)) {
    abort(403);
}

Gates en vistas Blade

Puedes usar la directiva @can para mostrar u ocultar elementos según los permisos del usuario:

blade
<article>
    <h1>{{ $post->title }}</h1>
    <p>{{ $post->content }}</p>

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

        <form method="POST" action="{{ route('posts.destroy', $post) }}">
            @csrf
            @method('DELETE')
            <button type="submit">Eliminar</button>
        </form>
    @endcan
</article>

También existe @cannot para el caso contrario:

blade
@cannot('update-post', $post)
    <p class="text-muted">No tienes permiso para editar este post.</p>
@endcannot

Policies: Gates organizados por modelo

Cuando tienes muchas reglas de autorización para un modelo, es mejor usar Policies. Una Policy es una clase que agrupa toda la lógica de autorización de un modelo.

Crea una Policy con Artisan:

bash
# Crear Policy para el modelo Post
php artisan make:policy PostPolicy --model=Post

Esto crea app/Policies/PostPolicy.php con métodos predefinidos:

php
<?php

declare(strict_types=1);

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    /**
     * ¿Puede ver la lista de posts?
     */
    public function viewAny(User $user): bool
    {
        return true; // Todos pueden ver la lista
    }

    /**
     * ¿Puede ver este post específico?
     */
    public function view(User $user, Post $post): bool
    {
        // Posts publicados: todos. Borradores: solo el autor
        return $post->is_published || $user->id === $post->user_id;
    }

    /**
     * ¿Puede crear posts?
     */
    public function create(User $user): bool
    {
        return true; // Cualquier usuario autenticado
    }

    /**
     * ¿Puede editar este post?
     */
    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }

    /**
     * ¿Puede eliminar este post?
     */
    public function delete(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }
}

Registrar Policies

Laravel 11+ detecta automáticamente las Policies si sigues la convención de nombres (PostPolicy para Post). Si necesitas registrarlas manualmente:

php
<?php

// app/Providers/AppServiceProvider.php

use App\Models\Post;
use App\Policies\PostPolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Gate::policy(Post::class, PostPolicy::class);
    }
}

Usar Policies en controladores

Una vez registrada la Policy, puedes usarla igual que los Gates. Laravel detecta automáticamente qué Policy usar según el modelo:

php
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\View\View;

class PostController extends Controller
{
    public function index(): View
    {
        // viewAny no necesita instancia del modelo
        Gate::authorize('viewAny', Post::class);

        $posts = Post::latest()->paginate(10);
        return view('posts.index', compact('posts'));
    }

    public function show(Post $post): View
    {
        // view necesita la instancia
        Gate::authorize('view', $post);

        return view('posts.show', compact('post'));
    }

    public function edit(Post $post): View
    {
        Gate::authorize('update', $post);

        return view('posts.edit', compact('post'));
    }

    public function update(Request $request, Post $post): RedirectResponse
    {
        Gate::authorize('update', $post);

        $post->update($request->validated());

        return redirect()->route('posts.show', $post);
    }

    public function destroy(Post $post): RedirectResponse
    {
        Gate::authorize('delete', $post);

        $post->delete();

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

Autorización en Form Requests

Los Form Requests tienen un método authorize() perfecto para verificar permisos antes de validar:

php
<?php

declare(strict_types=1);

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdatePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        $post = $this->route('post');

        return $this->user()->can('update', $post);
    }

    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:255'],
            'content' => ['required', 'string'],
        ];
    }
}

Si authorize() retorna false, Laravel devuelve automáticamente un error 403.

El método before en Policies

A veces quieres que ciertos usuarios (como administradores) puedan hacer todo. El método before() se ejecuta antes que cualquier otro método de la Policy:

php
<?php

declare(strict_types=1);

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    /**
     * Se ejecuta antes de cualquier otro método.
     * Si retorna true/false, ese es el resultado final.
     * Si retorna null, continúa al método específico.
     */
    public function before(User $user, string $ability): ?bool
    {
        if ($user->is_admin) {
            return true; // Administradores pueden todo
        }

        return null; // Continuar evaluación normal
    }

    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;
    }
}

Cuidado: Si before() retorna false, ningún otro método se evaluará. Retorna null para continuar la evaluación normal.

Autorización para usuarios invitados

Por defecto, Gates y Policies deniegan el acceso si no hay usuario autenticado. Si quieres permitir que usuarios invitados pasen la verificación, haz el parámetro $user nullable:

php
<?php

// En una Policy
public function view(?User $user, Post $post): bool
{
    // Posts públicos: cualquiera puede verlos
    if ($post->is_published) {
        return true;
    }

    // Posts privados: solo el autor (si está autenticado)
    return $user !== null && $user->id === $post->user_id;
}

// En un Gate
Gate::define('view-post', function (?User $user, Post $post): bool {
    return $post->is_published || ($user && $user->id === $post->user_id);
});

Middleware can

Puedes aplicar autorización directamente en las rutas usando el middleware can:

php
<?php

use App\Http\Controllers\PostController;
use App\Models\Post;
use Illuminate\Support\Facades\Route;

// Sin modelo (para create)
Route::get('/posts/create', [PostController::class, 'create'])
    ->middleware('can:create,App\Models\Post');

// Con modelo (usando route model binding)
Route::get('/posts/{post}/edit', [PostController::class, 'edit'])
    ->middleware('can:update,post');

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

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

Resumen

  • La autorización determina qué puede hacer un usuario, no quién es
  • Los Gates son closures simples para reglas puntuales
  • Las Policies agrupan reglas por modelo (recomendado)
  • Usa Gate::authorize() para lanzar 403 automáticamente
  • Usa @can en Blade para mostrar/ocultar elementos
  • El método before() permite bypass para administradores
  • Form Requests pueden incluir lógica de autorización en authorize()
  • El middleware can aplica autorización en rutas

Ejercicios

Ejercicio 1: Gate para administradores

Crea un Gate llamado access-admin que solo permita acceso a usuarios con el campo is_admin en true. Luego úsalo para proteger una ruta /admin/dashboard.

Ver solución
<?php

// app/Providers/AppServiceProvider.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
    {
        Gate::define('access-admin', function (User $user): bool {
            return $user->is_admin === true;
        });
    }
}
<?php

// routes/web.php

use App\Http\Controllers\Admin\DashboardController;
use Illuminate\Support\Facades\Route;

Route::middleware(['auth', 'can:access-admin'])->prefix('admin')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index'])
        ->name('admin.dashboard');
});
<?php

// app/Http/Controllers/Admin/DashboardController.php

declare(strict_types=1);

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Illuminate\View\View;

class DashboardController extends Controller
{
    public function index(): View
    {
        return view('admin.dashboard');
    }
}

Ejercicio 2: Policy para comentarios

Crea una CommentPolicy donde: cualquiera puede ver comentarios, solo usuarios autenticados pueden crear, y solo el autor puede editar o eliminar sus propios comentarios.

Ver solución
# Crear la Policy
php artisan make:policy CommentPolicy --model=Comment
<?php

// app/Policies/CommentPolicy.php

declare(strict_types=1);

namespace App\Policies;

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

class CommentPolicy
{
    /**
     * Cualquiera puede ver la lista de comentarios.
     */
    public function viewAny(?User $user): bool
    {
        return true;
    }

    /**
     * Cualquiera puede ver un comentario específico.
     */
    public function view(?User $user, Comment $comment): bool
    {
        return true;
    }

    /**
     * Solo usuarios autenticados pueden crear comentarios.
     */
    public function create(User $user): bool
    {
        return true;
    }

    /**
     * Solo el autor puede editar su comentario.
     */
    public function update(User $user, Comment $comment): bool
    {
        return $user->id === $comment->user_id;
    }

    /**
     * Solo el autor puede eliminar su comentario.
     */
    public function delete(User $user, Comment $comment): bool
    {
        return $user->id === $comment->user_id;
    }
}
<?php

// app/Http/Controllers/CommentController.php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Models\Comment;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\View\View;

class CommentController extends Controller
{
    public function update(Request $request, Comment $comment): RedirectResponse
    {
        Gate::authorize('update', $comment);

        $comment->update($request->validate([
            'content' => ['required', 'string', 'max:1000'],
        ]));

        return back()->with('success', 'Comentario actualizado.');
    }

    public function destroy(Comment $comment): RedirectResponse
    {
        Gate::authorize('delete', $comment);

        $comment->delete();

        return back()->with('success', 'Comentario eliminado.');
    }
}

Ejercicio 3: Autorización en vistas

Crea una vista que muestre un artículo. El botón "Editar" y "Eliminar" solo deben aparecer si el usuario actual es el autor. Además, si el artículo no está publicado, muestra un badge "Borrador" solo para el autor.

Ver solución
{{-- resources/views/articles/show.blade.php --}}

<x-app-layout>
    <article class="max-w-4xl mx-auto p-6">
        <header class="mb-6">
            <h1 class="text-3xl font-bold">{{ $article->title }}</h1>

            <div class="flex items-center gap-4 mt-2 text-gray-600">
                <span>Por {{ $article->author->name }}</span>
                <span>{{ $article->created_at->format('d/m/Y') }}</span>

                @can('update', $article)
                    @unless($article->is_published)
                        <span class="px-2 py-1 bg-yellow-100 text-yellow-800 rounded text-sm">
                            Borrador
                        </span>
                    @endunless
                @endcan
            </div>
        </header>

        <div class="prose max-w-none">
            {!! $article->content !!}
        </div>

        @can('update', $article)
            <footer class="mt-8 pt-6 border-t flex gap-4">
                <a
                    href="{{ route('articles.edit', $article) }}"
                    class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
                >
                    Editar
                </a>

                <form
                    method="POST"
                    action="{{ route('articles.destroy', $article) }}"
                    onsubmit="return confirm('¿Eliminar este artículo?')"
                >
                    @csrf
                    @method('DELETE')
                    <button
                        type="submit"
                        class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
                    >
                        Eliminar
                    </button>
                </form>
            </footer>
        @endcan
    </article>
</x-app-layout>

¿Te está gustando el curso?

Tenemos cursos premium con proyectos reales, soporte personalizado y certificado.

Descubrir cursos premium