Di web, otorisasi berakhir di dua tempat: controller menolak request dan Blade menyembunyikan tombol. API tidak punya Blade — konsumen API hanya menerima status code dan JSON — dan identitas pengguna tidak dibawa session melainkan token. Konsekuensinya menarik: satu pengguna bisa memiliki beberapa token dengan hak berbeda, jadi authorization API punya dua lapis pertanyaan — “siapa pemilik token” dan “apa yang boleh dilakukan token ini”. Laravel Sanctum menjawab lapisan kedua lewat token abilities: daftar string yang tertanam pada token saat dibuat. Chapter ini mempraktikkan pemeriksaan ability di middleware, bentuk JSON dari respons 403, dan hasil uji nyata empat skenario token. Pembahasan mendalam tentang manajemen token ada di section Laravel Sanctum ; di sini kita ambil bagian otorisasinya saja.
Token Abilities Sekilas #
Method createToken() menerima nama token plus array ability yang diizinkan:
$token = $budi->createToken('token-budi', ['posts-update']);
echo $token->plainTextToken;Hanya plainTextToken yang ditampilkan sekali saat pembuatan — inilah string yang dikirim konsumen API pada header Authorization: Bearer .... Token tersebut boleh mengubah tulisan, tetapi tidak membawa ability lain. Pengecekan pada sisi server memakai method tokenCan() milik trait HasApiTokens:
$request->user()->tokenCan('posts-update');Method tersebut mengembalikan true hanya bila request berjalan lewat token yang membawa ability dimaksud. Perlu dicatat: pengguna yang diotentikasi lewat session web tidak memiliki access token, sehingga tokenCan() otomatis mengembalikan false — perilaku yang tepat, karena ability memang milik token, bukan user. Dengan kata lain, dua token milik user yang sama bisa punya hak yang berbeda total; pemisahan ini yang membuat pengecekan ability menjadi lapisan authorization tersendiri di atas policy.
Middleware Pemeriksaan Ability #
Pola paling rapi adalah middleware khusus yang menerima nama ability sebagai parameter — konsep yang sama dengan scope pada OAuth:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckTokenAbility
{
public function handle(Request $request, Closure $next, string $ability): Response
{
if (! $request->user()?->tokenCan($ability)) {
abort(403, "Token tidak memiliki ability {$ability}.");
}
return $next($request);
}
}Middleware ini kita daftarkan sebagai alias ability di bootstrap/app.php, lalu dipasang pada route API bersama auth:sanctum:
use App\Http\Controllers\PostApiController;
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->group(function (): void {
Route::put('/posts/{post}', [PostApiController::class, 'update'])
->middleware('ability:posts-update')
->name('api.posts.update');
Route::delete('/posts/{post}', [PostApiController::class, 'destroy'])
->name('api.posts.destroy');
});Middleware menangani lapisan token; lapisan kepemilikan tetap dipegang controller melalui $this->authorize('update', $post) seperti biasa. Kombinasi keduanya berarti token harus membawa ability dan pengguna harus pemiliknya.
Bentuk JSON dari 403 dan 401 #
Karena konsumen API membaca JSON, penolakan juga berbicara JSON. Feature test berikut menguji tiga skenario pada PUT /api/posts/{post} — token sempurna milik pemilik, token tanpa ability, dan token ber-ability milik orang lain:
$budi = User::factory()->create([
'name' => 'Budi', 'email' => '[email protected]', 'role' => 'penulis',
]);
$post = Post::create([
'user_id' => $budi->id,
'judul' => 'Authorization API',
'isi' => 'Isi tulisan Budi.',
]);
$tokenPenuh = $budi->createToken('token-budi', ['posts-update']);
$tokenKosong = $budi->createToken('token-budi', []);
$ok = $this->withToken($tokenPenuh->plainTextToken)
->putJson("/api/posts/{$post->id}", ['judul' => 'Judul via API']);
echo 'ability posts-update PUT /api/posts/{post} status: '.$ok->status()."\n";
echo 'body: '.$ok->getContent()."\n";Output yang Dihasilkan:
ability posts-update PUT /api/posts/{post} status: 200
body: {"status":"diperbarui","judul":"Judul via API"}Dua skenario penolakannya:
$siti = User::factory()->create([
'name' => 'Siti', 'email' => '[email protected]', 'role' => 'penulis',
]);
$ditolakToken = $this->withToken($tokenKosong->plainTextToken)
->putJson("/api/posts/{$post->id}", ['judul' => 'Serobot via API']);
echo 'ability kosong PUT /api/posts/{post} status: '.$ditolakToken->status()."\n";
echo 'body: '.$ditolakToken->getContent()."\n";
$tokenSiti = $siti->createToken('token-siti', ['posts-update']);
$ditolakPemilik = $this->withToken($tokenSiti->plainTextToken)
->putJson("/api/posts/{$post->id}", ['judul' => 'Serobot via API']);
echo 'siti + posts-update PUT /api/posts/{post} status: '.$ditolakPemilik->status()."\n";
echo 'body: '.$ditolakPemilik->getContent()."\n";Output yang Dihasilkan:
ability kosong PUT /api/posts/{post} status: 403
body: {
"message": "Token tidak memiliki ability posts-update."
}
siti + posts-update PUT /api/posts/{post} status: 403
body: {
"message": "This action is unauthorized."
}Dua penolakan itu datang dari lapisan berbeda dan pesannya membuktikannya: middleware ability menolak token kosong dengan pesan eksplisit, sedangkan policy menolak token Siti dengan pesan standar “This action is unauthorized.” — artinya token sah, kemampuan cukup, tetapi bukan pemiliknya.
Pesan Kustom dari Policy #
Ketika policy memakai Response::deny('...') seperti method delete milik PostPolicy, pesan kustomnya ikut mengalir ke JSON:
$tokenSiti = $siti->createToken('token-siti', ['posts-update']);
$ditolakHapus = $this->withToken($tokenSiti->plainTextToken)
->deleteJson("/api/posts/{$post->id}");
echo 'siti DELETE /api/posts/{post} status: '.$ditolakHapus->status()."\n";
echo 'body: '.$ditolakHapus->getContent()."\n";Output yang Dihasilkan:
siti DELETE /api/posts/{post} status: 403
body: {
"message": "Hanya pemilik tulisan yang dapat menghapus."
}Terakhir, request tanpa token sama sekali ditangkap auth:sanctum sebelum tiba di ability maupun policy — statusnya 401, bukan 403:
tanpa token PUT /api/posts/{post} status: 401
body: {"message":"Unauthenticated."}Otorisasi kini utuh untuk web dan API. Tapi ada satu kelas kerentanan yang tidak muncul dari pemeriksaan yang kurang — melainkan dari pemeriksaan yang lupa dipasang: pengguna mengubah data milik orang lain hanya dengan mengganti angka di URL. Chapter berikutnya membongkar celah IDOR dan menutupnya dengan policy serta route model binding.