API authentication; modern web uygulamalarının temel güvenlik katmanıdır. Laravel Sanctum, SPA'lar ve mobil uygulamalar için token tabanlı kimlik doğrulama sağlar — Passport'un karmaşıklığı olmadan. Bu derste Laravel Sanctum'la tam bir API auth sistemi inşa edeceksin.
Sanctum vs Passport
// PLAINTEXT //
Sanctum: Passport:
✓ SPA + mobil için ideal ✓ OAuth2 sunucu gerektiğinde
✓ Kurulum 5 dakika ✗ Karmaşık kurulum
✓ Token + Cookie auth ✗ JWT sadece
✓ Lightweight ✗ Fazla özellikEğer sadece kendi frontend'in veya mobil uygulamanı yetkilendiriyorsan Sanctum'u seç.
Kurulum
// BASH //
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate// PHP //
// app/Models/User.php — HasApiTokens trait ekle
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
}// PHP //
// bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
$middleware->api(prepend: [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
]);
})Kayıt ve Giriş Endpoint'leri
// PHP //
// 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
{
public function register(Request $request)
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users'],
'password' => ['required', 'min:8', 'confirmed'],
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
$token = $user->createToken(
name: 'mobile-app',
abilities: ['read', 'write'],
expiresAt: now()->addDays(30),
);
return response()->json([
'user' => $user->only(['id', 'name', 'email']),
'access_token' => $token->plainTextToken,
'token_type' => 'Bearer',
], 201);
}
public function login(Request $request)
{
$request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
$user = User::where('email', $request->email)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['Kimlik bilgileri hatalı.'],
]);
}
// Mevcut tokenları iptal et (opsiyonel — tek oturum)
$user->tokens()->delete();
$token = $user->createToken(
name: $request->device_name ?? 'api',
abilities: $this->resolveAbilities($user),
);
return response()->json([
'access_token' => $token->plainTextToken,
'token_type' => 'Bearer',
'expires_at' => now()->addDays(30)->toIso8601String(),
]);
}
public function logout(Request $request)
{
// Sadece mevcut tokenı iptal et
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Başarıyla çıkış yapıldı.']);
}
public function logoutAll(Request $request)
{
// Tüm tokenları iptal et (tüm cihazlardan çıkış)
$request->user()->tokens()->delete();
return response()->json(['message' => 'Tüm cihazlardan çıkış yapıldı.']);
}
public function me(Request $request)
{
return response()->json($request->user());
}
private function resolveAbilities(User $user): array
{
return $user->is_admin ? ['*'] : ['read', 'write'];
}
}Route Tanımları
// PHP //
// routes/api.php
use App\Http\Controllers\Api\AuthController;
Route::prefix('auth')->group(function () {
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::post('/logout-all', [AuthController::class, 'logoutAll']);
Route::get('/me', [AuthController::class, 'me']);
});
});
// Korumalı API rotaları
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('posts', PostController::class);
Route::get('/dashboard', DashboardController::class);
});Token Abilities (Yetki Sistemi)
// PHP //
// Token oluştururken yetkiler ata
$token = $user->createToken('mobile', ['posts:read', 'posts:create']);
// Controller'da yetki kontrolü
public function store(Request $request)
{
// Token bu yetke sahip değilse 403
if (! $request->user()->tokenCan('posts:create')) {
abort(403, 'Bu işlem için yetkiniz yok.');
}
// ...
}
// Middleware ile yetki kontrolü
Route::middleware(['auth:sanctum', 'abilities:posts:create'])->group(function () {
Route::post('/posts', [PostController::class, 'store']);
});Token Yenileme (Refresh)
Sanctum'un native refresh sistemi yoktur, ama basit bir pattern ekleyebilirsin:
// PHP //
// AuthController'a ekle
public function refresh(Request $request)
{
$user = $request->user();
// Mevcut tokenı sil
$user->currentAccessToken()->delete();
// Yeni token üret
$newToken = $user->createToken(
name: 'refreshed',
expiresAt: now()->addDays(30),
);
return response()->json([
'access_token' => $newToken->plainTextToken,
'expires_at' => now()->addDays(30)->toIso8601String(),
]);
}SPA (Cookie) Authentication
// PHP //
// config/sanctum.php — izin verilen domainler
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
Sanctum::currentApplicationUrlWithPort()
))),// JAVASCRIPT //
// Next.js / React — SPA auth akışı
// 1. CSRF token al
await fetch('http://localhost:8000/sanctum/csrf-cookie', {
credentials: 'include',
});
// 2. Giriş yap
const response = await fetch('http://localhost:8000/api/auth/login', {
method: 'POST',
credentials: 'include', // cookie gönder
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': getCookie('XSRF-TOKEN'), // CSRF koruması
},
body: JSON.stringify({ email, password }),
});Test Yazımı
// PHP //
// tests/Feature/AuthTest.php
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AuthTest extends TestCase
{
use RefreshDatabase;
public function test_kullanici_kayit_olabilir(): void
{
$response = $this->postJson('/api/auth/register', [
'name' => 'Test Kullanıcı',
'email' => 'test@example.com',
'password' => 'Gizli123!',
'password_confirmation' => 'Gizli123!',
]);
$response->assertStatus(201)
->assertJsonStructure(['user', 'access_token', 'token_type']);
$this->assertDatabaseHas('users', ['email' => 'test@example.com']);
}
public function test_gecersiz_bilgilerle_giris_yapilamaz(): void
{
User::factory()->create(['email' => 'test@example.com', 'password' => bcrypt('dogru')]);
$this->postJson('/api/auth/login', [
'email' => 'test@example.com',
'password' => 'yanlis',
])->assertStatus(422);
}
public function test_korunan_rotalara_token_olmadan_erisim_engellenir(): void
{
$this->getJson('/api/auth/me')->assertStatus(401);
}
public function test_kullanici_cikis_yapabilir(): void
{
$user = User::factory()->create();
$token = $user->createToken('test')->plainTextToken;
$this->withToken($token)
->postJson('/api/auth/logout')
->assertOk();
// Token artık geçersiz
$this->withToken($token)
->getJson('/api/auth/me')
->assertStatus(401);
}
}Güvenlik Önlemleri
// PHP //
// Rate limiting — giriş denemelerini sınırla
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->throttleApi();
})
// veya rotaya özel
Route::post('/auth/login', [AuthController::class, 'login'])
->middleware('throttle:5,1'); // 1 dakikada 5 deneme
// Token boyutu optimizasyonu — token listesini temizle
// App\Console\Commands\PruneExpiredTokens.php
Artisan::command('sanctum:prune-expired --hours=24', function () {
$this->call(PruneExpiredTokens::class, ['--hours' => 24]);
})->daily();Özet
Laravel Sanctum; kayıt, giriş, çıkış ve token yetkisi için gereken her şeyi sağlar. createToken() ile ability atama, tokenCan() ile kontrol ve tokens()->delete() ile çıkış — üç satır API auth'un özüdür. SPA için cookie tabanlı auth, mobil için Bearer token, ikisi aynı sistemde çalışır.