1. LATAR BELAKANG
Setelah membangun API yang robust dengan validasi, error handling, dan response yang konsisten, tiba saatnya untuk membahas aspek paling kritis dalam pengembangan API: keamanan dan autentikasi. API yang kita bangun sejauh ini bersifat publik—siapa pun bisa mengakses, membuat, mengupdate, atau menghapus data. Tentunya ini tidak aman untuk aplikasi production.
Pada hari kelima minggu ini, saya mempelajari bagaimana mengamankan API menggunakan Laravel Sanctum. Sanctum adalah paket autentikasi ringan untuk API yang memungkinkan kita mengeluarkan token API kepada pengguna. Setiap request ke endpoint yang dilindungi harus menyertakan token ini untuk membuktikan identitas pengguna.
Mengapa Sanctum? Karena:
- Sederhana - Mudah diintegrasikan
- Ringan - Tidak membawa banyak fitur yang tidak diperlukan
- Fleksibel - Bisa untuk API token maupun autentikasi SPA
- First-party - Dikembangkan langsung oleh tim Laravel
Dengan Sanctum, kita bisa membedakan akses antara pengguna biasa, admin, atau bahkan tamu (guest), serta melindungi endpoint tertentu agar hanya bisa diakses oleh pengguna yang memiliki hak akses yang sesuai.
2. ALAT DAN BAHAN
2.1 Perangkat Lunak
- Laravel 11 - Project API yang sudah berjalan (dari artikel sebelumnya)
- Laravel Sanctum - Paket autentikasi API (akan diinstall)
- Postman/Thunder Client - Untuk testing endpoint dengan token
- Visual Studio Code - Editor kode
- Git Bash/Terminal - Untuk menjalankan perintah Artisan
2.2 Perangkat Keras
- Laptop dengan spesifikasi standar
- Koneksi internet (untuk instalasi paket)
3. PEMBAHASAN
3.1 Apa itu Laravel Sanctum?
Laravel Sanctum adalah paket autentikasi yang dirancang khusus untuk API. Sanctum bekerja dengan cara mengeluarkan API token kepada pengguna yang berhasil login. Token ini kemudian harus disertakan dalam setiap request ke endpoint yang dilindungi, biasanya melalui header Authorization: Bearer {token}.
Cara Kerja Sanctum:
- Pengguna mengirim email dan password ke endpoint login
- Sistem memverifikasi kredensial
- Jika valid, sistem membuat token unik dan mengembalikannya ke pengguna
- Pengguna menyimpan token ini (di mobile app/local storage)
- Setiap request berikutnya, pengguna menyertakan token di header
- Sistem memverifikasi token sebelum memproses request
3.2 Instalasi dan Konfigurasi Sanctum
3.2.1 Install Paket Sanctum
# Install sanctum via composercomposer require laravel/sanctum# Publish migration dan konfigurasiphp artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"# Jalankan migration untuk membuat tabel personal_access_tokensphp artisan migrate
3.2.2 Konfigurasi Model User
Tambahkan trait HasApiTokens ke model User:
<?php// File: app/Models/User.phpnamespace App\Models;// use Illuminate\Contracts\Auth\MustVerifyEmail;use Illuminate\Database\Eloquent\Factories\HasFactory;use Illuminate\Foundation\Auth\User as Authenticatable;use Illuminate\Notifications\Notifiable;use Laravel\Sanctum\HasApiTokens; // Tambahkan iniclass User extends Authenticatable{use HasApiTokens, HasFactory, Notifiable; // Tambahkan HasApiTokens/*** The attributes that are mass assignable.** @var array<int, string>*/protected $fillable = ['name','email','password',];/*** The attributes that should be hidden for serialization.** @var array<int, string>*/protected $hidden = ['password','remember_token',];/*** The attributes that should be cast.** @var array<string, string>*/protected $casts = ['email_verified_at' => 'datetime','password' => 'hashed',];/*** Relasi ke posts*/public function posts(){return $this->hasMany(Post::class);}}
3.2.3 Konfigurasi Middleware di Kernel
Pastikan middleware auth:sanctum sudah terdaftar. Biasanya sudah otomatis, tapi bisa dicek di app/Http/Kernel.php:
// File: app/Http/Kernel.phpprotected $routeMiddleware = [// ...'auth' => \App\Http\Middleware\Authenticate::class,'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,'can' => \Illuminate\Auth\Middleware\Authorize::class,// ...];
3.2.4 Konfigurasi di routes/api.php
<?php// File: routes/api.phpuse Illuminate\Support\Facades\Route;use App\Http\Controllers\Api\AuthController;use App\Http\Controllers\Api\PostController;use App\Http\Controllers\Api\UserController;// ===== PUBLIC ROUTES (Tidak perlu token) =====Route::prefix('auth')->group(function () {Route::post('/register', [AuthController::class, 'register']);Route::post('/login', [AuthController::class, 'login']);});// Posts yang bisa diakses publik (hanya baca)Route::get('/posts', [PostController::class, 'index']);Route::get('/posts/{post}', [PostController::class, 'show']);// ===== PROTECTED ROUTES (Perlu token) =====Route::middleware('auth:sanctum')->group(function () {// User profileRoute::get('/user', [AuthController::class, 'user']);Route::post('/logout', [AuthController::class, 'logout']);// CRUD Posts (kecuali index dan show sudah publik)Route::apiResource('posts', PostController::class)->except(['index', 'show']);// Manajemen tokenRoute::get('/tokens', [AuthController::class, 'tokens']);Route::delete('/tokens/{token}', [AuthController::class, 'revokeToken']);});
3.3 Membuat Controller Autentikasi
3.3.1 AuthController
php artisan make:controller Api/AuthController<?php// File: app/Http/Controllers/Api/AuthController.phpnamespace App\Http\Controllers\Api;use App\Http\Controllers\Controller;use App\Http\Requests\LoginRequest;use App\Http\Requests\RegisterRequest;use App\Models\User;use App\Traits\ApiResponseTrait;use Illuminate\Http\Request;use Illuminate\Support\Facades\Auth;use Illuminate\Support\Facades\Hash;use Illuminate\Validation\ValidationException;class AuthController extends Controller{use ApiResponseTrait;/*** Register user baru* POST /api/auth/register*/public function register(RegisterRequest $request){// Data sudah tervalidasi dari Form Request$user = User::create(['name' => $request->name,'email' => $request->email,'password' => Hash::make($request->password),'role' => 'user' // Default role]);// Buat token untuk user yang baru register (otomatis login)$token = $user->createToken('auth_token')->plainTextToken;return $this->successResponse(['user' => ['id' => $user->id,'name' => $user->name,'email' => $user->email,'role' => $user->role,'created_at' => $user->created_at->format('d M Y')],'token' => $token,'token_type' => 'Bearer'], 'Registrasi berhasil', 201);}/*** Login user* POST /api/auth/login*/public function login(LoginRequest $request){$request->authenticate(); // Method dari Form Request$user = $request->user();// Hapus token lama jika ingin hanya 1 token aktif// $user->tokens()->delete();// Buat token baru$token = $user->createToken('auth_token')->plainTextToken;// Simpan log login (opsional)activity()->log('User login');return $this->successResponse(['user' => ['id' => $user->id,'name' => $user->name,'email' => $user->email,'role' => $user->role,'email_verified' => !is_null($user->email_verified_at)],'token' => $token,'token_type' => 'Bearer','expires_in' => config('sanctum.expiration') // jika diatur], 'Login berhasil');}/*** Logout user (hapus token yang digunakan)* POST /api/auth/logout*/public function logout(Request $request){// Hapus token yang sedang digunakan$request->user()->currentAccessToken()->delete();// Atau hapus semua token user// $request->user()->tokens()->delete();return $this->successResponse(null, 'Logout berhasil');}/*** Get authenticated user* GET /api/user*/public function user(Request $request){$user = $request->user()->load(['posts' => function($query) {$query->latest()->limit(5);}]);return $this->successResponse(['id' => $user->id,'name' => $user->name,'email' => $user->email,'role' => $user->role,'avatar' => $user->avatar ? url($user->avatar) : null,'joined_at' => $user->created_at->diffForHumans(),'stats' => ['total_posts' => $user->posts()->count(),'total_comments' => $user->comments()->count(),'total_likes' => $user->likes()->count()],'recent_posts' => $user->posts->map(function($post) {return ['id' => $post->id,'title' => $post->title,'slug' => $post->slug,'created_at' => $post->created_at->format('d M Y')];})], 'Data user berhasil diambil');}/*** Dapatkan daftar token user* GET /api/tokens*/public function tokens(Request $request){$tokens = $request->user()->tokens()->get()->map(function($token) {return ['id' => $token->id,'name' => $token->name,'abilities' => $token->abilities,'last_used_at' => $token->last_used_at ? $token->last_used_at->diffForHumans() : null,'created_at' => $token->created_at->format('d M Y H:i')];});return $this->successResponse($tokens, 'Daftar token berhasil diambil');}/*** Revoke (cabut) token tertentu* DELETE /api/tokens/{token}*/public function revokeToken($tokenId){$user = request()->user();// Cari token milik user ini$token = $user->tokens()->where('id', $tokenId)->first();if (!$token) {return $this->notFoundResponse('Token tidak ditemukan');}// Hapus token$token->delete();return $this->successResponse(null, 'Token berhasil dicabut');}/*** Ganti password* POST /api/change-password*/public function changePassword(Request $request){$request->validate(['current_password' => 'required|current_password','new_password' => 'required|min:8|confirmed']);$user = $request->user();$user->password = Hash::make($request->new_password);$user->save();// Opsional: logout dari semua device// $user->tokens()->delete();return $this->successResponse(null, 'Password berhasil diubah');}}
3.3.2 Form Request untuk Autentikasi
php artisan make:request RegisterRequestphp artisan make:request LoginRequest
RegisterRequest:
<?php// File: app/Http/Requests/RegisterRequest.phpnamespace App\Http\Requests;use Illuminate\Foundation\Http\FormRequest;use Illuminate\Contracts\Validation\Validator;use Illuminate\Http\Exceptions\HttpResponseException;class RegisterRequest extends FormRequest{public function authorize(): bool{return true; // Semua orang boleh register}public function rules(): array{return ['name' => 'required|string|max:255','email' => 'required|string|email|max:255|unique:users','password' => 'required|string|min:8|confirmed','password_confirmation' => 'required|string|min:8','terms' => 'accepted' // Setuju syarat & ketentuan];}public function messages(): array{return ['name.required' => 'Nama lengkap wajib diisi','email.required' => 'Email wajib diisi','email.email' => 'Format email tidak valid','email.unique' => 'Email sudah terdaftar','password.required' => 'Password wajib diisi','password.min' => 'Password minimal 8 karakter','password.confirmed' => 'Konfirmasi password tidak cocok','terms.accepted' => 'Anda harus menyetujui syarat dan ketentuan'];}protected function failedValidation(Validator $validator){throw new HttpResponseException(response()->json(['success' => false,'message' => 'Validasi gagal','errors' => $validator->errors()], 422));}}
LoginRequest:
<?php// File: app/Http/Requests/LoginRequest.phpnamespace App\Http\Requests;use Illuminate\Foundation\Http\FormRequest;use Illuminate\Support\Facades\Auth;use Illuminate\Validation\ValidationException;class LoginRequest extends FormRequest{public function authorize(): bool{return true;}public function rules(): array{return ['email' => 'required|string|email','password' => 'required|string','device_name' => 'nullable|string|max:255' // Untuk memberi nama token];}public function messages(): array{return ['email.required' => 'Email wajib diisi','email.email' => 'Format email tidak valid','password.required' => 'Password wajib diisi'];}/*** Attempt to authenticate the request's credentials.** @throws \Illuminate\Validation\ValidationException*/public function authenticate(): void{if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {throw ValidationException::withMessages(['email' => ['Email atau password salah.'],]);}}}
3.4 Proteksi Endpoint dengan Middleware
3.4.1 Middleware auth:sanctum
Middleware auth:sanctum akan memeriksa apakah request menyertakan token yang valid. Jika tidak, akan mengembalikan response 401 Unauthorized.
// Di routes/api.phpRoute::middleware('auth:sanctum')->group(function () {// Semua route di sini dilindungiRoute::apiResource('posts', PostController::class)->except(['index', 'show']);});
3.4.2 Membuat Middleware Kustom untuk Role
php artisan make:middleware CheckRole<?php// File: app/Http/Middleware/CheckRole.phpnamespace App\Http\Middleware;use Closure;use Illuminate\Http\Request;use Symfony\Component\HttpFoundation\Response;class CheckRole{/*** Handle an incoming request.*/public function handle(Request $request, Closure $next, ...$roles): Response{$user = $request->user();if (!$user) {return response()->json(['success' => false,'message' => 'Unauthenticated'], 401);}// Cek apakah user memiliki role yang diizinkanif (!in_array($user->role, $roles)) {return response()->json(['success' => false,'message' => 'Anda tidak memiliki akses ke resource ini'], 403);}return $next($request);}}
Daftarkan middleware di Kernel:
// File: app/Http/Kernel.phpprotected $routeMiddleware = [// ...'role' => \App\Http\Middleware\CheckRole::class,];
Penggunaan di routes:
Route::middleware(['auth:sanctum', 'role:admin'])->group(function () {Route::apiResource('users', UserController::class);Route::get('/dashboard/stats', [DashboardController::class, 'stats']);});Route::middleware(['auth:sanctum', 'role:admin,editor'])->group(function () {Route::post('/posts/{post}/publish', [PostController::class, 'publish']);});
3.5 Policy untuk Otorisasi (Authorization)
Policy memungkinkan kita mengatur logika otorisasi secara terpusat, misalnya hanya pemilik post yang boleh mengupdate atau menghapus post-nya.
3.5.1 Membuat Policy
php artisan make:policy PostPolicy --model=Post<?php// File: app/Policies/PostPolicy.phpnamespace App\Policies;use App\Models\Post;use App\Models\User;use Illuminate\Auth\Access\HandlesAuthorization;class PostPolicy{use HandlesAuthorization;/*** Admin bisa melakukan apa saja*/public function before(User $user, $ability){if ($user->role === 'admin') {return true;}}/*** Determine whether the user can update the post.*/public function update(User $user, Post $post): bool{return $user->id === $post->user_id;}/*** Determine whether the user can delete the post.*/public function delete(User $user, Post $post): bool{return $user->id === $post->user_id;}/*** Determine whether the user can publish the post.*/public function publish(User $user, Post $post): bool{return $user->id === $post->user_id || $user->role === 'editor';}}
3.5.2 Mendaftarkan Policy
// File: app/Providers/AuthServiceProvider.phpuse App\Models\Post;use App\Policies\PostPolicy;class AuthServiceProvider extends ServiceProvider{protected $policies = [Post::class => PostPolicy::class,];// ...}
3.5.3 Menggunakan Policy di Controller
<?php// File: app/Http/Controllers/Api/PostController.phpuse App\Models\Post;use App\Http\Requests\UpdatePostRequest;class PostController extends Controller{public function update(UpdatePostRequest $request, Post $post){// Menggunakan Policy$this->authorize('update', $post);$validated = $request->validated();$post->update($validated);return $this->successResponse(new PostResource($post),'Post berhasil diupdate');}public function destroy(Post $post){// Menggunakan Policy$this->authorize('delete', $post);$post->delete();return $this->deletedResponse('Post berhasil dihapus');}public function publish(Post $post){// Method kustom di policy$this->authorize('publish', $post);$post->update(['is_published' => true,'published_at' => now()]);return $this->successResponse(new PostResource($post),'Post berhasil dipublikasikan');}}
3.6 Membuat Resource untuk User
<?php// File: app/Http/Resources/UserResource.phpnamespace App\Http\Resources;use Illuminate\Http\Request;use Illuminate\Http\Resources\Json\JsonResource;class UserResource extends JsonResource{public function toArray(Request $request): array{return ['id' => $this->id,'name' => $this->name,'username' => $this->username ?? $this->email,'email' => $this->email,'role' => $this->role,'avatar' => $this->avatar ? url($this->avatar) : null,'bio' => $this->bio,'joined_at' => $this->created_at->format('d M Y'),'joined_ago' => $this->created_at->diffForHumans(),'is_verified' => !is_null($this->email_verified_at),'stats' => ['posts_count' => $this->whenCounted('posts'),'comments_count' => $this->whenCounted('comments'),'likes_received' => $this->whenCounted('likesReceived')]];}}
3.7 Testing Endpoint dengan Postman
3.7.1 Register User
Request:
POST /api/auth/registerContent-Type: application/json{ "name": "John Doe", "email": "john@example.com", "password": "password123", "password_confirmation": "password123", "terms": true}
Response (201):
{"success": true,"message": "Registrasi berhasil","data": {"user": {"id": 1,"name": "John Doe","email": "john@example.com","role": "user","created_at": "24 Feb 2026"},"token": "1|a1b2c3d4e5f6g7h8i9j0...","token_type": "Bearer"}}
3.7.2 Login User
Request:
POST /api/auth/loginContent-Type: application/json{ "email": "john@example.com", "password": "password123"}
Response (200):
{"success": true,"message": "Login berhasil","data": {"user": {"id": 1,"name": "John Doe","email": "john@example.com","role": "user","email_verified": false},"token": "2|k1l2m3n4o5p6q7r8s9t0...","token_type": "Bearer"}}
3.7.3 Akses Protected Endpoint (dengan token)
Request:
POST /api/postsAuthorization: Bearer 2|k1l2m3n4o5p6q7r8s9t0...Content-Type: application/json{ "title": "Post dengan Auth", "content": "Ini adalah post yang dibuat setelah login", "category_id": 1}
Response (201):
{"success": true,"message": "Post berhasil dibuat","data": {"id": 5,"title": "Post dengan Auth","content": "Ini adalah post yang dibuat setelah login","user_id": 1,"created_at": "2 minutes ago"}}
3.7.4 Akses Tanpa Token (401)
Request:
POST /api/postsContent-Type: application/json{ "title": "Post tanpa Auth"}
Response (401):
{"message": "Unauthenticated."}
3.7.5 Akses dengan Token Invalid (401)
Request:
POST /api/postsAuthorization: Bearer token_salah_123Content-Type: application/json
Response (401):
{"message": "Unauthenticated."}
3.7.6 Akses dengan Role Tidak Sesuai (403)
Request:
DELETE /api/users/2 (Endpoint khusus admin)Authorization: Bearer valid_token_user_biasa
Response (403):
{"success": false,"message": "Anda tidak memiliki akses ke resource ini"}
3.7.7 Logout
Request:
POST /api/auth/logoutAuthorization: Bearer 2|k1l2m3n4o5p6q7r8s9t0...
Response (200):
{"success": true,"message": "Logout berhasil","data": null}
Token yang digunakan sekarang tidak valid lagi.
3.8 Fitur Lanjutan Sanctum
3.8.1 Token Abilities (Scopes)
Sanctum memungkinkan kita memberi "kemampuan" spesifik pada token:
// Membuat token dengan abilities terbatas$token = $user->createToken('mobile-token', ['posts:read', 'profile:read'])->plainTextToken;// Cek abilities di middleware/controllerif ($user->tokenCan('posts:create')) {// Boleh membuat post}
Penggunaan di routes:
Route::middleware(['auth:sanctum', 'abilities:posts:create'])->group(function () {Route::post('/posts', [PostController::class, 'store']);});Route::middleware(['auth:sanctum', 'ability:posts:read,profile:read'])->group(function () {Route::get('/posts', [PostController::class, 'index']);Route::get('/user', [AuthController::class, 'user']);});
3.8.2 Multiple Token per User
Satu user bisa memiliki banyak token untuk device yang berbeda:
// Login dari mobile$mobileToken = $user->createToken('mobile-app')->plainTextToken;// Login dari web$webToken = $user->createToken('web-session')->plainTextToken;// Login dari API third-party$apiToken = $user->createToken('api-integration', ['posts:read'])->plainTextToken;
3.8.3 Token Expiration
Atur masa berlaku token di config/sanctum.php:
// config/sanctum.php'expiration' => 60 * 24, // Token berlaku 24 jam (dalam menit)
Atau secara manual saat membuat token:
use Laravel\Sanctum\NewAccessToken;$token = $user->createToken('auth_token',['*'],now()->addHours(24) // Expire dalam 24 jam);
3.9 Menambahkan Fitur "Remember Me"
Modifikasi method login untuk mendukung remember token:
public function login(LoginRequest $request){$credentials = $request->only('email', 'password');$remember = $request->boolean('remember');if (!Auth::attempt($credentials, $remember)) {return $this->errorResponse('Email atau password salah', 401);}$user = Auth::user();// Jika remember=true, buat token dengan masa berlaku lebih panjang$expiration = $remember ? now()->addDays(30) : now()->addDays(1);$token = $user->createToken($request->device_name ?? 'auth_token',['*'],$expiration)->plainTextToken;return $this->successResponse(['user' => new UserResource($user),'token' => $token,'token_type' => 'Bearer','expires_at' => $expiration->format('Y-m-d H:i:s')], 'Login berhasil');}
3.10 Kendala dan Solusi
Kendala yang Dihadapi:
- Lupa menambahkan trait HasApiTokens di User model
- Solusi: Selalu cek model User setelah install sanctum
Token tidak dikirim dengan benar di header
- Solusi: Pastikan format header
Authorization: Bearer {token}(ada spasi setelah Bearer) Bingung membedakan autentikasi vs otorisasi
- Autentikasi = siapa kamu? (login/logout) → auth:sanctum
- Otorisasi = apa yang boleh kamu lakukan? → Policy & Role
Token bocor atau perlu di-revoke
- Solusi: Sediakan endpoint untuk melihat dan mencabut token
User bisa akses data orang lain
- Solusi: Gunakan Policy untuk memeriksa kepemilikan data
4. KESIMPULAN
Laravel Sanctum memberikan solusi autentikasi yang lengkap namun tetap sederhana untuk API kita. Dengan Sanctum, kita bisa:
- Mengelola Registrasi dan Login
- User bisa mendaftar dan login dengan aman
- Password di-hash menggunakan Bcrypt
Membuat dan Mengelola Token
- Setiap login menghasilkan token unik
- Token bisa memiliki abilities (scope)
- Token bisa di-revoke (logout)
Melindungi Endpoint
- Middleware
auth:sanctumuntuk endpoint yang perlu autentikasi - Response 401 untuk request tanpa token
- Response 403 untuk akses tidak sah
Otorisasi Berbasis Role dan Policy
- Middleware role untuk membedakan akses admin/user
- Policy untuk aturan spesifik (hanya pemilik yang boleh edit)
Dengan semua komponen ini, API kita sekarang:
- Aman - Hanya user terautentikasi yang bisa mengakses endpoint tertentu
- Terotorisasi - User hanya bisa mengakses data miliknya sendiri
- Auditable - Bisa melacak token siapa yang mengakses apa
- Profesional - Siap digunakan untuk aplikasi production
Pada artikel tambahan selanjutnya, kita akan mempelajari cara mendeploy REST API Laravel ke Laravel Cloud agar bisa diakses secara publik.
5. DAFTAR PUSTAKA
- Laravel Official Documentation. (n.d.). Laravel 12.x Documentation - Sanctum. https://laravel.com/docs/12.x/sanctum
- Laravel Official Documentation. (n.d.). Laravel 12.x Documentation - Authentication. https://laravel.com/docs/12.x/authentication
- Laravel Official Documentation. (n.d.). Laravel 12.x Documentation - Authorization. https://laravel.com/docs/12.x/authorization
- Laravel Official Documentation. (n.d.). Laravel 12.x Documentation - API Authentication (Sanctum). https://laravel.com/docs/12.x/sanctum#api-token-authentication

0 Komentar