PHP / LARAVEL // LARAVELD::03 ORTA
18m READCOMPLETION: 83%ID::PHP-201
YAKINDA

LARAVEL SANCTUM API AUTHENTICATION

Token tabanlı API auth, SPA guard ve session yönetimi

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 özellik

Eğ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.