Instalasi di chapter sebelumnya sudah menyiapkan tabel dan trait; sekarang kita mencetak token pertama dan mengirim request dengannya. Fokus chapter ini adalah siklus hidup personal access token: bagaimana createToken() menghasilkan plaintext yang hanya tampil sekali, apa yang sebenarnya tersimpan di database, dan bagaimana header Authorization: Bearer diverifikasi oleh middleware auth:sanctum. Semua output di bawah diambil dari sandbox yang benar-benar menjalankan kodenya.
Route API dan Registrasinya #
Contoh kita memakai satu endpoint terlindungi di routes/api.php. Pada Laravel 13 aplikasi baru, berkas ini tidak ada sampai kita mendaftarkannya — lewat php artisan install:api atau manual di bootstrap/app.php:
<?php
// bootstrap/app.php (cuplikan)
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
// ...
)
// ...
->create();Dengan begitu semua route di routes/api.php otomatis berprefix /api. Endpoint profil kita dilindungi satu middleware:
<?php
// routes/api.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/profil', fn (Request $request) => [
'nama' => $request->user()->name,
'email' => $request->user()->email,
])->middleware('auth:sanctum');Middleware auth:sanctum meminta request terautentikasi lewat guard sanctum — guard yang mencari bearer token di header.
Mencetak Token dengan createToken #
Trait HasApiTokens dari chapter Instalasi dan Konfigurasi
memberi model User method createToken('nama-token'). Method tersebut mengembalikan objek NewAccessToken yang membawa dua properti publik: plainTextToken (nilai token untuk client) dan accessToken (model PersonalAccessToken yang mewakili baris database):
<?php
$user = User::create([
'name' => 'Budi',
'email' => '[email protected]',
'password' => 'rahasia-budi',
]);
$token = $user->createToken('mobile-budi');
echo $token->plainTextToken; // dikirim ke client, sekali saja
echo $token->accessToken->id; // 1
echo json_encode($token->accessToken->abilities); // ["*"]
Output yang Dihasilkan:
1|PTV3wAeRGkvwVC0IljOCDygdDpnnMMYL8WHGzLIva9c50933
1
["*"]Plaintext berformat id|acak: angka 1 adalah ID baris di database, disusul 40 karakter acak — total 50 karakter pada contoh di atas. Mari kita telaah kode di atas baris per baris:
User::create([...]): Membuat user Budi dengan kredensial eksplisit agar output dapat diprediksi.$user->createToken('mobile-budi'): Method dari traitHasApiTokensmembuat baris di tabelpersonal_access_tokensdengan labelmobile-budidan abilities bawaan['*'].$token->plainTextToken: Properti publikNewAccessTokenyang berisi stringid|acak— satu-satunya representasi token yang bisa dibaca manusia.$token->accessToken: Model EloquentPersonalAccessTokenuntuk baris yang baru dibuat, lengkap dengan relasitokenablekembali ke user.
Ketika client mengirim token ini, sanctum guard memecah string di tanda pipa, mengambil 40 karakter acaknya, menghitung hash('sha256', ...), lalu memuat baris dengan ID tersebut dan mencocokkan hash-nya. Hanya pencocokan kedua-duanya — ID benar dan hash benar — yang membuat request dianggap sah; mengarang bagian acak berarti mencari 40 karakter yang hash-nya pas dengan baris tertentu.
Kapan token sebaiknya dicetak? Pada aplikasi mobile, momen yang wajar adalah tepat setelah login: endpoint login memvalidasi email dan password, lalu mengembalikan plaintext token sebagai bagian dari response. Pola endpoint POST /login untuk client mobile akan kita lihat utuh saat membangun manajemen token di chapter API Token Management
.
Apa yang Tersimpan di Database #
Mari kita intip baris yang baru dibuat:
<?php
use Illuminate\Support\Facades\DB;
$row = DB::table('personal_access_tokens')->first();
echo json_encode([
'id' => $row->id,
'name' => $row->name,
'token' => $row->token,
'abilities' => json_decode($row->abilities),
'expires_at' => $row->expires_at,
], JSON_PRETTY_PRINT);Output yang Dihasilkan:
{
"id": 1,
"name": "mobile-budi",
"token": "b674b9c659dd31bbdb9c1fae648de5bf8013f70080e1993265d64808d9a03a2e",
"abilities": [
"*"
],
"expires_at": null
}Kolom token berisi hash SHA-256, bukan plaintext. Kita bisa membuktikannya langsung: hash('sha256', 'PTV3wAeRGkvwVC0IljOCDygdDpnnMMYL8WHGzLIva9c50933') — bagian setelah tanda pipa — menghasilkan persis nilai b674b9...03a2e di atas (verifikasi di sandbox mengembalikan bool(true)). Konsekuensinya penting: setelah response pembuatan token lewat, tidak ada jalan untuk membaca ulang nilainya — bahkan oleh kita sendiri sebagai pengembang. Kolom last_used_at juga patut diamati: nilainya null saat token baru dibuat, lalu diperbarui Laravel pada setiap request yang diautentikasi token tersebut.
Autentikasi Request Bearer #
Client menyimpan plaintext lalu melampirkannya di setiap request. Kita buktikan kedua sisi perilakunya — tanpa token dan dengan token — lewat request HTTP sungguhan:
<?php
$request = Illuminate\Http\Request::create('/api/profil', 'GET', [], [], [], [
'HTTP_ACCEPT' => 'application/json',
'HTTP_AUTHORIZATION' => 'Bearer '.$token->plainTextToken,
]);
$response = $app->handle($request);Output yang Dihasilkan:
### GET /api/profil tanpa header Authorization
HTTP 401
{"message":"Unauthenticated."}
### GET /api/profil dengan Bearer token sah
HTTP 200
{"nama":"Budi","email":"[email protected]"}Request tanpa token ditolak 401 dengan bentuk JSON yang sama seperti yang kita kenal di chapter API Authentication
. Setelah token sah diterima, $request->user() otomatis terisi dan currentAccessToken() membuka akses ke baris token yang sedang dipakai — properti yang sama berguna untuk mengaudit abilities di controller. Perilaku ini kita kunci sebagai feature test:
<?php
// tests/Feature/Sanctum/BearerTest.php (ringkas)
public function test_tanpa_token_ditolak_401(): void
{
$this->getJson('/api/profil')
->assertStatus(401)
->assertJson(['message' => 'Unauthenticated.']);
}
public function test_bearer_token_sah_diterima_200(): void
{
$user = User::factory()->create([
'name' => 'Budi',
'email' => '[email protected]',
]);
$plain = $user->createToken('mobile-budi')->plainTextToken;
$this->getJson('/api/profil', [
'Authorization' => 'Bearer '.$plain,
])->assertStatus(200)->assertJson([
'nama' => 'Budi',
'email' => '[email protected]',
]);
}Output yang Dihasilkan:
PASS Tests\Feature\Sanctum\BearerTest
✓ tanpa token ditolak 401
✓ token salah ditolak 401
✓ bearer token sah diterima 200
Tests: 3 passed (6 assertions)Token yang berlaku penuh seperti ini bisa mengakses segalanya milik Budi. Sering kali kita ingin lebih hati-hati — token yang hanya boleh membaca, atau hanya boleh membuat tulisan. Pembatasan itulah pekerjaan token abilities di chapter berikutnya, dan dokumentasi Sanctum menjadi rujukan resminya.