Belajar Laravel #11: Authorization — Gate & Policy, Pagar Kedua Setelah Login

Di episode sebelumnya saya belajar bagaimana Laravel mengenali siapa yang sedang login — itu disebut autentikasi. Hari ini saya lanjut satu level: setelah tahu siapa penggunanya, bagaimana caranya memastikan pengguna itu berhak melakukan aksi tertentu? Inilah yang disebut otorisasi. Saya baru sadar, dua konsep ini sering dicampur, padahal bedanya tegas: autentikasi menjawab “kamu siapa?”, otorisasi menjawab “kamu boleh apa?”. Di Laravel, otorisasi diurus oleh dua alat: Gate dan Policy. Ini catatan saya.

Diagram alur autentikasi vs otorisasi di Laravel
Autentikasi dulu, baru otorisasi. Keduanya berjalan berurutan di setiap request.

Pertama, Bedakan Dulu: Authentication vs Authorization

Analogi yang paling membekas buat saya: autentikasi itu KTP, otorisasi itu izin masuk ruangan. KTP membuktikan siapa saya. Tapi punya KTP tidak otomatis membuat saya berhak masuk ruang server — itu urusan tersendiri. Di aplikasi web, seorang pengguna yang sudah login (terautentikasi) belum tentu boleh mengedit postingan milik orang lain (terotorisasi). Episode 10 kemarin membahas KTP-nya (guard, hash, alur login). Sekarang saya membahas pintu-pintunya.

Gate: Cek Izin Ringan dengan Closure

Gate adalah cara paling sederhana untuk mendefinisikan izin. Bentuknya closure yang menerima user dan mengembalikan true/false. Dokumentasi resmi menyebutnya “closure-based approach” — cocok untuk kemampuan yang tidak terikat pada model tertentu, misalnya “boleh melihat dashboard admin” atau “boleh mengakses laporan billing”. Gate biasanya didaftarkan di App\Providers\AppServiceProvider:

use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::define('view-admin-dashboard', function (User $user) {
        return $user->is_admin;
    });
}

Lalu di controller atau Blade, saya cek dengan facade Gate atau helper can:

if (Gate::allows('view-admin-dashboard')) {
    // tampilkan dashboard
}

// di Blade:
@can('view-admin-dashboard')
    <a href="/admin">Buka Admin</a>
@endcan

Kalau user tidak berhak dan saya ingin langsung menghentikan request, ada Gate::authorize — kalau gagal, Laravel otomatis melempar 403:

Gate::authorize('view-admin-dashboard');

Policy: Aturan Per Model yang Lebih Terstruktur

Kalau izinnya berkutat di satu model — misalnya “bolehkah user ini mengupdate Post dengan ID 5?” — Gate akan cepat berantakan. Di situ Laravel menyediakan Policy: kelas yang mengelompokkan logika otorisasi per model, mirip controller yang mengelompokkan rute. Documentation-nya memakai analogi “gates like routes, policies like controllers”. Buat policy dengan artisan:

php artisan make:policy PostPolicy --model=Post

Perintah dengan --model=Post sekaligus mengisi metode bawaan: viewAny, view, create, update, delete, restore, forceDelete. Contoh metode update yang memastikan hanya pemilik post yang bisa mengedit:

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

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

Laravel memetakan policy ke model secara otomatis selama nama kelasnya {NamaModel}Policy dan modelnya memakai trait App\Models\Post standar. Kalau perlu mapping manual, bisa didaftarkan di AuthServiceProvider (meski di Laravel 11+ provider ini tidak lagi otomatis dibuat — auto-discovery sudah cukup).

Memakai Policy di Controller dan Blade

Di controller, cara yang saya pakai paling sering: helper authorize di dalam method, dengan model sebagai argumen. Instance user diambil otomatis dari guard yang sedang aktif:

use App\Models\Post;

public function update(Request $request, Post $post)
{
    $this->authorize('update', $post);

    // hanya sampai sini kalau user adalah pemilik post
    $post->update($request->validated());
}

Catatan penting dari yang saya baca di dokumentasi Laravel 13: controller di versi terbaru tidak lagi mewarisi AuthorizesRequests secara otomatis seperti dulu, jadi sebagian developer memilih memanggil Gate::authorize('update', $post) secara eksplisit. Keduanya sah — yang penting konsisten. Di Blade, pengecekannya begini:

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

Before dan After Callbacks di Policy

Yang menarik, Policy punya hook before dan after yang bisa saya pakai untuk logika lintas-method. Misalnya, kalau saya ingin admin selalu diizinkan melakukan apa saja tanpa mengecek method satu per satu:

public function before(User $user, string $ability): ?bool
{
    if ($user->is_admin) {
        return true; // admin lolos semua pengecekan
    }
    return null; // lanjut ke method spesifik
}

public function after(User $user, string $ability, mixed $result): void
{
    // log setiap keputusan otorisasi, berguna untuk audit
    Log::info("User {$user->id} — ability: {$ability} — result: " . ($result ? 'granted' : 'denied'));
}

before dijalankan pertama kali sebelum method spesifik. Kalau mengembalikan true atau false, method spesifik dilewati. Kalau mengembalikan null, pengecekan dilanjutkan ke method yang sesuai. after dijalankan setelah keputusan final, cocok untuk logging atau audit trail.

Otorisasi Inline: Middleware vs Method

Saya menemukan ada dua cara “inline” untuk menerapkan otorisasi di route: menggunakan middleware('can:') di level route atau $this->authorize() di level controller. Keduanya punya tempat masing-masing:

// Option 1: middleware di route — otorisasi sebelum controller dijalankan
Route::put('/posts/{post}', [PostController::class, 'update'])
    ->middleware('can:update,post');

// Option 2: authorize di dalam controller — lebih fleksibel
public function update(Request $request, Post $post)
{
    $this->authorize('update', $post);
    // ...
}

Middleware can: berguna kalau izin sudah jelas sejak awal dan tidak perlu data tambahan dari request. Pengecekan di controller lebih fleksibel kalau ada logika sebelum otorisasi, misalnya validasi input dulu atau mengambil data dari request. Saya pribadi cenderung pakai middleware kalau syaratnya sederhana, dan $this->authorize() kalau ada dependensi tambahan.

Authorization di Blade: @can, @cannot, @guest

Di Blade, Laravel menyediakan direktif khusus untuk otorisasi yang membuat template tetap rapi:

@can('create', App\Models\Post::class)
    <a href="{{ route('posts.create') }}">Tulis Postingan Baru</a>
@endcan

@cannot('update', $post)
    <p>Anda tidak memiliki akses untuk mengedit postingan ini.</p>
@endcannot

@guest
    <p>Silakan login terlebih dahulu untuk mengakses fitur ini.</p>
@endguest

@can menampilkan blok jika user boleh. @cannot kebalikannya — menampilkan blok jika user tidak boleh. @guest menampilkan konten hanya untuk pengguna yang belum login. Tiga direktif ini menghemat banyak @if bersarang yang kalau ditulis manual akan cepat tidak terbaca.

Resource Ownership: Pola Kepemilikan Data

Satu pola yang saya temui berulang kali: memastikan user hanya bisa mengelola data miliknya sendiri. Ini dasar dari otorisasi berbasis kepemilikan. Berikut contoh lengkap di policy untuk model Post:

public function viewAny(User $user): bool
{
    return true; // semua user bisa melihat daftar post
}

public function view(User $user, Post $post): bool
{
    return true; // semua user bisa melihat satu post (public)
}

public function create(User $user): bool
{
    return true; // semua user terautentikasi boleh menulis
}

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

Pola kuncinya: viewAny dan create tidak menerima model karena belum ada data spesifik. update dan delete menerima model sebagai parameter kedua — di sinilah saya membandingkan $user->id dengan $post->user_id. Kalau tidak cocok, otorisasi ditolak. Pola ini saya sebut "resource ownership gate" — menjadi fondasi setiap CRUD yang aman.

Multi-Guard Authorization

Dalam aplikasi yang punya lebih dari satu guard — misalnya web untuk user biasa dan admin untuk administrator — Laravel secara otomatis menggunakan guard yang sedang aktif saat menentukan instance user untuk otorisasi. Ini berarti policy saya tidak perlu menulis logika branching untuk membedakan admin dan user biasa di setiap method. Yang perlu saya pastikan: guard di config/auth.php sudah benar, dan middleware route sudah menentukan guard yang tepat:

// route untuk admin
Route::middleware(['auth:admin'])->group(function () {
    Route::get('/admin/dashboard', [AdminDashboardController::class, 'index']);
});

// route untuk user biasa
Route::middleware(['auth:web'])->group(function () {
    Route::put('/posts/{post}', [PostController::class, 'update'])
        ->middleware('can:update,post');
});

Saat guard admin aktif, instance User yang di-pass ke policy adalah model admin. Guard web memberikan model user biasa. Sistem otorisasi Laravel tidak peduli guard mana yang dipakai — yang penting user yang dikirim adalah instance model yang benar.

Kapan Memakai Gate, Kapan Policy?

Dari pengalaman belajar saya, patokan praktisnya begini:

Tabel Perbandingan: Gate vs Policy

AspekGatePolicy
BentukClosure / class-basedClass (1 model = 1 policy)
Cocok untukIzin global tanpa model (admin dashboard, export)Izin per model (edit Post, hapus Comment)
Daftarkan diAppServiceProvider > boot()Otomatis via naming convention
ParameterUser sajaUser + Model instance
Contoh izin"Bisa lihat laporan billing""Bisa update Post ini"
ScalingCepat untuk < 5 izinRapi untuk banyak izin per model
Before/AfterGate::before() / Gate::after()before() / after() di class policy

Selain itu, ada juga Gate::before untuk administrator super yang "menembus" semua aturan, dan Gate::forUser($user) kalau ingin mengecek atas nama user tertentu (misalnya dari command Artisan/queue). Keduanya penting untuk proyek yang mulai tumbuh.

Catatan Pribadi: Momen Memahami Beda Authentication dan Authorization

Ada momen spesifik saat saya benar-benar paham kenapa otorisasi berbeda dari autentikasi. Saya sedang membangin fitur CRUD postingan, dan saya menyadari: aplikasi saya sudah bisa login, tapi siapa pun yang login bisa menghapus postingan siapa pun. Saya panik — autentikasi sudah jalan, tapi tidak ada yang memblokir aksi destruktif. Di situlah saya paham: autentikasi hanya memastikan "kamu pasti si A", tapi tidak menjawab "si A boleh melakukan X terhadap Y". Keduanya harus jalan beriringan seperti dua layer keamanan berturut-turut — KTP di pintu depan, kartu akses di pintu server. Tanpa yang kedua, pintu depan yang kokoh tidak ada gunanya.

Kesimpulan Saya

Otorisasi adalah pagar kedua setelah login. Tanpa pagar ini, aplikasi yang "sudah bisa login" masih bocor: siapa pun yang login bisa menghapus data siapa pun. Gate memberi pagar cepat untuk kemampuan umum, policy memberi pagar rapi per model. Menurut saya kombinasi keduanya (Gate untuk aksi global, Policy untuk kepemilikan data) adalah pola yang paling masuk akal untuk proyek kecil sampai menengah. Episode depan saya mau coba praktik: bikin CRUD sederhana yang menggabungkan migration, Eloquent, form, dan otorisasi ini sekaligus.

Sumber

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *

Situs ini menggunakan Akismet untuk mengurangi spam. Pelajari bagaimana data komentar Anda diproses