Home Paket Belajar Bootcamp Instruktur

Tutorial Laravel Authorization #8 - Spatie Permission untuk API dengan Laravel Sanctum

Pelajari sistem Authorization di Laravel dari nol hingga implementasi dalam studi kasus nyata melalui 5 episode terstruktur. Ebook ini membahas konsep Authentication vs Authorization, Gates, Policies, fitur lanjutan, hingga menggabungkan seluruh konsep dalam sebuah aplikasi blog. Setiap materi dilengkapi penjelasan konsep, contoh kode siap pakai, tabel perbandingan, dan praktik terbaik agar mudah dipahami oleh developer Laravel pemula maupun menengah.

✅ Telah dilihat 25 kali

Rating: 5.00 ⭐

... 11 June 2026, 14:31

Setelah membaca episode ini, teman-teman diharapkan bisa:

  • Menginstall dan mengkonfigurasi Laravel Sanctum untuk autentikasi API
  • Melindungi route API berdasarkan role dan permission Spatie
  • Mengembalikan data role & permission di response JSON (login response)
  • Membuat middleware custom untuk proteksi API yang lebih fleksibel

1. Kenapa Sanctum, Bukan Passport?

  Sanctum Passport
Kompleksitas Ringan, simpel Berat, OAuth2 penuh
Cocok untuk SPA, mobile app, token sederhana Third-party OAuth, public API
Setup 15 menit 1-2 jam
Rekomendasi Laravel 11 ✅ Default Hanya jika butuh OAuth2

Untuk kebanyakan project (SPA + mobile app), Sanctum sudah lebih dari cukup.


2. Instalasi Sanctum

Di Laravel 11, Sanctum sudah terinstall secara default. Cukup jalankan:

php artisan install:api

Perintah ini akan:

  • Membuat file routes/api.php
  • Menjalankan migration tabel personal_access_tokens
  • Menambahkan api guard ke config/auth.php

Tambahkan Trait ke Model User

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;           // ← tambahkan
use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasApiTokens, HasRoles; // ← keduanya

    // ...
}

3. Buat AuthController untuk API

php artisan make:controller Api/AuthController

app/Http/Controllers/Api/AuthController.php:

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    // Login — kembalikan token + data role & permission
    public function login(Request $request)
    {
        $request->validate([
            'email'    => 'required|email',
            'password' => 'required|string',
        ]);

        $user = User::where('email', $request->email)->first();

        if (!$user || !Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['Email atau password salah.'],
            ]);
        }

        // Hapus token lama (opsional — satu user satu token aktif)
        $user->tokens()->delete();

        // Buat token baru dengan abilities berdasarkan permissions Spatie
        $permissions = $user->getAllPermissions()->pluck('name')->toArray();
        $token = $user->createToken(
            name: 'api-token',
            abilities: $permissions  // ← permission jadi abilities token
        )->plainTextToken;

        return response()->json([
            'token' => $token,
            'user'  => [
                'id'          => $user->id,
                'name'        => $user->name,
                'email'       => $user->email,
                'roles'       => $user->getRoleNames(),         // ['admin', 'editor']
                'permissions' => $user->getAllPermissions()     // semua permission dari role
                                      ->pluck('name'),
            ],
        ]);
    }

    // Logout — cabut token aktif
    public function logout(Request $request)
    {
        $request->user()->currentAccessToken()->delete();

        return response()->json(['message' => 'Berhasil logout.']);
    }

    // Data user yang sedang login
    public function me(Request $request)
    {
        $user = $request->user()->load('roles', 'permissions');

        return response()->json([
            'id'          => $user->id,
            'name'        => $user->name,
            'email'       => $user->email,
            'roles'       => $user->getRoleNames(),
            'permissions' => $user->getAllPermissions()->pluck('name'),
        ]);
    }
}

4. Setup Routes API

routes/api.php:

<?php

use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\PostController;
use App\Http\Controllers\Api\Admin\UserController;
use Illuminate\Support\Facades\Route;

// Route publik — tidak perlu token
Route::post('/login', [AuthController::class, 'login']);

// Route yang butuh token Sanctum
Route::middleware('auth:sanctum')->group(function () {

    Route::post('/logout', [AuthController::class, 'logout']);
    Route::get('/me', [AuthController::class, 'me']);

    // Post — proteksi per endpoint berdasarkan permission
    Route::get('/posts', [PostController::class, 'index']);
    Route::get('/posts/{post}', [PostController::class, 'show']);

    Route::post('/posts', [PostController::class, 'store'])
        ->middleware('permission:post.create');

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

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

    // Admin — hanya role admin
    Route::middleware('role:admin')->prefix('admin')->group(function () {
        Route::apiResource('users', UserController::class);
    });
});

Catatan: Middleware role: dan permission: dari Spatie bekerja langsung di route API juga — tidak perlu konfigurasi tambahan.


5. PostController untuk API

php artisan make:controller Api/PostController --api

app/Http/Controllers/Api/PostController.php:

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::with('user:id,name')
            ->where('status', 'published')
            ->latest()
            ->paginate(10);

        return response()->json($posts);
    }

    public function show(Post $post)
    {
        // Cek permission via Policy — tetap bisa dipakai di API
        $this->authorize('view', $post);

        return response()->json([
            'data' => $post->load('user:id,name'),
        ]);
    }

    public function store(Request $request)
    {
        // Permission sudah dicek di middleware route
        $validated = $request->validate([
            'title'   => 'required|string|max:255',
            'content' => 'required|string',
            'status'  => 'required|in:draft,published',
        ]);

        $post = $request->user()->posts()->create($validated);

        return response()->json([
            'message' => 'Postingan berhasil dibuat.',
            'data'    => $post,
        ], 201);
    }

    public function update(Request $request, Post $post)
    {
        $validated = $request->validate([
            'title'   => 'sometimes|string|max:255',
            'content' => 'sometimes|string',
            'status'  => 'sometimes|in:draft,published',
        ]);

        $post->update($validated);

        return response()->json([
            'message' => 'Postingan berhasil diperbarui.',
            'data'    => $post->fresh(),
        ]);
    }

    public function destroy(Post $post)
    {
        $post->delete();

        return response()->json([
            'message' => 'Postingan berhasil dihapus.',
        ]);
    }
}

6. Menangani Error 403 & 401 dengan Response JSON

Secara default, Laravel mengembalikan HTML saat terjadi error 403/401. Untuk API, kita perlu JSON.

app/Exceptions/Handler.php — atau bootstrap/app.php di Laravel 11:

// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {

    // Kembalikan JSON untuk request API
    $exceptions->render(function (\Illuminate\Auth\AuthenticationException $e, $request) {
        if ($request->is('api/*') || $request->expectsJson()) {
            return response()->json([
                'message' => 'Unauthenticated. Token tidak valid atau sudah kedaluwarsa.',
            ], 401);
        }
    });

    $exceptions->render(function (\Illuminate\Auth\Access\AuthorizationException $e, $request) {
        if ($request->is('api/*') || $request->expectsJson()) {
            return response()->json([
                'message' => 'Forbidden. teman-teman tidak punya izin untuk aksi ini.',
            ], 403);
        }
    });

    $exceptions->render(function (\Spatie\Permission\Exceptions\UnauthorizedException $e, $request) {
        if ($request->is('api/*') || $request->expectsJson()) {
            return response()->json([
                'message' => 'Forbidden. Role atau permission tidak mencukupi.',
                'required' => $e->getRequiredRoles(), // role apa yang dibutuhkan
            ], 403);
        }
    });
})

7. Contoh Request & Response

Login

POST /api/login
Content-Type: application/json

{
    "email": "[email protected]",
    "password": "password"
}

Response 200:

{
    "token": "1|abcdefghijklmnopqrstuvwxyz...",
    "user": {
        "id": 2,
        "name": "Editor",
        "email": "[email protected]",
        "roles": ["editor"],
        "permissions": [
            "post.viewAny",
            "post.view",
            "post.create",
            "post.update",
            "post.delete"
        ]
    }
}

Akses endpoint tanpa permission

DELETE /api/admin/users/3
Authorization: Bearer 1|abcdefgh...

Response 403 (user adalah editor, bukan admin):

{
    "message": "Forbidden. Role atau permission tidak mencukupi.",
    "required": ["admin"]
}

Akses tanpa token

GET /api/me

Response 401:

{
    "message": "Unauthenticated. Token tidak valid atau sudah kedaluwarsa."
}

8. Mengecek Permission di Frontend (SPA/Mobile)

Karena login response sudah menyertakan array permissions, frontend bisa langsung menyimpannya dan menggunakannya untuk kontrol tampilan — tanpa perlu request tambahan.

Contoh di Vue/React:

// Simpan setelah login
const { token, user } = await api.post('/login', credentials)
localStorage.setItem('token', token)
localStorage.setItem('permissions', JSON.stringify(user.permissions))

// Helper cek permission
const can = (permission) => {
    const permissions = JSON.parse(localStorage.getItem('permissions') ?? '[]')
    return permissions.includes(permission)
}

// Penggunaan di komponen
if (can('post.delete')) {
    // tampilkan tombol hapus
}

Ringkasan Episode

  • Laravel Sanctum di Laravel 11 diinstall dengan php artisan install:api
  • Token Sanctum bisa diberi abilities berdasarkan permissions Spatie — menyatukan dua sistem
  • Middleware role: dan permission: dari Spatie bekerja langsung di route API
  • Tangani error 403/401 di bootstrap/app.php agar selalu kembalikan JSON untuk request API
  • Login response sertakan roles dan permissions agar frontend bisa kontrol tampilan tanpa request tambahan

Preview Episode Berikutnya

Di Episode 9 — episode penutup series ini — kita akan membangun mini project API lengkap: autentikasi Sanctum + Spatie Permission + endpoint CRUD, dilengkapi dengan Postman collection siap pakai untuk testing semua endpoint.

Daftar eBook