Session lifetime 1 yıl olmasına rağmen birkaç saatte 419 hatası alınıyor. Tüm sistem detaylı analiz edildi.
Session lifetime 1 yıl (525600 dakika) olarak ayarlanmış AMA birkaç saat içinde kullanıcılar 419 CSRF Token Mismatch hatası alıyor. Redis session storage kullanılıyor ama tenant isolation eksik.
• ixtif.com kullanıcıları
• muzibu.com kullanıcıları
• Özellikle Livewire kullanan sayfalar
• Admin panel form işlemleri
Kullanıcılar siteye giriş yaptıktan birkaç saat sonra, bir butona tıkladığında "419 Page Expired" hatası alıyor. Sayfayı yenilemek zorunda kalıyorlar.
Her kullanıcının bir "oturum bileti" var. Bu bilet:
Kullanıcının tarayıcısında bilet var ama sunucuda yok! Sistem "Bu bilet geçersiz!" diyor → 419 hatası.
5 farklı sebep tespit edildi:
Her tenant için ayrı "bilet deposu" oluşturacağız. ixtif.com'un biletleri "ixtif_session_ABC123" olacak, muzibu.com'nin biletleri "muzibu_session_XYZ789" olacak. Böylece biletler birbirine karışmayacak ve kaybolma riski azalacak.
# Mevcut Durum
config('database.redis.options.prefix') = "" (BOŞ!)
config('session.connection') = null
config('session.store') = null
# Redis'teki session key'ler:
fBt6WvBGBSQWXM2DJU0Ge7ny94k4RGOXv6jGyFSL ← ixtif.com session
N5nNqyhWoU9INK4eVy4A6H4vcPkvEhkoyarsGW9S ← muzibu.com session
ZmyddagAv1lJ2Vmet8bPcW0ulsULAKOY9LlmvWrz ← ixtif.com session
⚠️ SORUN: Prefix yok, sadece plain session ID!
Risk: Eğer iki tenant aynı session ID oluşturursa (çok düşük ihtimal ama mümkün), session'lar çakışır. Bir kullanıcı diğer tenant'a ait session'a erişebilir (güvenlik riski!).
# config/session.php
'driver' => 'redis',
'connection' => null, ❌ Default connection kullanıyor (DB 0)
# config/database.php
'redis' => [
'default' => ['database' => 0], ← SESSION BURDA
'cache' => ['database' => 1],
'tenant_isolated' => ['database' => 2],
'critical_operations' => ['database' => 3],
]
⚠️ SORUN: Session ve cache aynı DB'de değil AMA
Session için özel connection tanımlanmamış!
Risk: Default connection kullanılıyor, eğer başka bir servis de DB 0'ı kullanırsa key collision riski var. Ayrıca session'lar için özel timeout/retry ayarları yapılamıyor.
# Redis Memory Status used_memory: 12.70 MB maxmemory: 0 (unlimited) maxmemory_policy: noeviction evicted_keys: 0 # Session Stats Total keys DB 0: 798 Session keys: ~300-400 (tahmin) Avg TTL: 511421032 saniye (~16 yıl!) ✅ İYİ: Memory yeterli, eviction yok ✅ İYİ: TTL çok uzun (session expire olmuyor) ❌ SORUN: TTL neden 16 yıl? (525600 dakika = 31536000 saniye = 1 yıl olmalıydı!)
Analiz: Redis memory sorun değil. Ama TTL'nin 16 yıl olması garip - muhtemelen bazı session'lar manuel set edilmiş veya farklı bir mekanizma kullanılmış.
Session Key: fBt6WvBGBSQWXM2DJU0Ge7ny94k4RGOXv6jGyFSL
Deserialized Content:
{
"current_tenant": {
"id": 2,
"title": "ixtif.com",
"tenancy_db_name": "tenant_ixtif",
"domains": ["ixtif.com", "ixtif.com.tr"]
},
"tenant_locale": "tr",
"_token": "fBt6WvBGBSQWXM2DJU0Ge7ny94k4RGOXv6jGyFSL",
"_previous": {
"url": "https://ixtif.com/shop/pdf/..."
},
"_flash": {
"old": [],
"new": []
}
}
✅ Tenant bilgisi session'da mevcut
✅ CSRF token session'da saklanıyor
✅ Previous URL tracking çalışıyor
current_tenant objesi var (tenant aware çalışıyor)
Dosya: app/Http/Middleware/VerifyCsrfToken.php:70-74
protected function addCookieToResponse($request, $response)
{
$config = config('session');
// Tenant context'te domain'i host'tan al
if (tenant()) {
$host = $request->getHost();
// Subdomain desteği için nokta prefix ekle
$config['domain'] = '.' . $host; ← RUNTIME SET!
}
$response->headers->setCookie(
new Cookie(
'XSRF-TOKEN',
$request->session()->token(),
$this->availableAt(60 * $config['lifetime']),
$config['path'],
$config['domain'], ← .ixtif.com veya .muzibu.com
...
)
);
}
SORUN: Cookie domain runtime'da set ediliyor. Eğer kullanıcı "ixtif.com" → "www.ixtif.com" arası geçiş yaparsa (RemoveWwwPrefix middleware var ama), cookie domain mismatch olabilir.
# ixtif.com domain=.ixtif.com Max-Age=31536000 (1 yıl) ✅ secure=true httponly=true samesite=lax # muzibu.com domain=.muzibu.com Max-Age=31536000 (1 yıl) ✅ secure=true httponly=true samesite=lax ✅ Cookie lifetime doğru (1 yıl) ✅ Security flags doğru ✅ Domain prefix doğru (.domain.com)
Session regeneration yapılan yerler (grep sonucu):
app/Http/Controllers/Api/AuthController.php:
→ $request->session()->regenerateToken();
app/Http/Controllers/Api/Auth/AuthController.php:
→ $request->session()->regenerate(); (3 kez!)
→ $request->session()->regenerateToken(); (2 kez!)
app/Http/Controllers/Auth/AuthenticatedSessionController.php:
→ $request->session()->regenerate();
→ $request->session()->regenerateToken();
app/Http/Controllers/CsrfController.php:
→ $request->session()->regenerateToken(); (Manuel refresh endpoint!)
app/Http/Controllers/ProfileController.php:
→ $request->session()->regenerateToken();
app/Http/Middleware/CheckApproval.php:
→ $request->session()->regenerateToken();
TOPLAM: 10+ farklı yerde session regeneration!
SORUN: Çok fazla regeneration var! Her regeneration:
regenerate() → Yeni session ID, eski session SİLİNİR!regenerateToken() → Sadece CSRF token yenilenir (session ID aynı)
// app/Http/Middleware/CheckApproval.php
if (!auth()->user()->is_approved) {
auth()->logout();
$request->session()->invalidate();
$request->session()->regenerateToken(); ← HER ONAYSIZ GİRİŞTE TOKEN YENİLENİYOR!
return redirect()->route('approval.pending');
}
SORUN: Onaylanmamış kullanıcı her sayfa değiştirdiğinde token yenileniyor. Eğer eski token'lı bir form varsa → 419 hatası!
1. Sayfa yüklenir → CSRF token oluşturulur
- _token: "ABC123..."
- XSRF-TOKEN cookie set edilir
2. Livewire component render → Token blade'de gömülü
@csrf →
3. Kullanıcı butona tıklar → POST /livewire/update
- Headers: X-CSRF-TOKEN: "ABC123..."
- Body: _token: "ABC123..."
4. Laravel VerifyCsrfToken middleware:
- Session'dan token al: $request->session()->token()
- Request'ten token al: $request->header('X-CSRF-TOKEN')
- Karşılaştır: hash_equals($sessionToken, $requestToken)
- Eşleşmezse → 419 hatası!
⚠️ SORUN: Session kaybolursa $sessionToken boş olur → 419!
Livewire her request'te token yenilemiyor (doğru davranış). Ama eğer:
→ Livewire component'teki eski token geçersiz olur → 419 hatası!
bootstrap/app.php - Web Middleware Group: 1. RemoveWwwPrefix ← www → non-www redirect 2. InitializeTenancy ← Tenant belirleme (FIRST!) 3. RedisHealthCheckMiddleware ← Redis connection kontrolü 4. CheckThemeStatus ← Tema kontrolü 5. SecurityHeaders ← Security headers 6. DatabasePoolMiddleware ← Connection pooling 7. ResourceTrackingMiddleware ← Resource tracking 8. RootOnlyDebugbar ← Debugbar 9. LivewireJsonSanitizer ← UTF-8 sanitization 10. UnderConstructionProtection ← Muzibu şifre koruması Laravel Auto Middleware (implicit): - StartSession ← Session başlatma - VerifyCsrfToken ← CSRF kontrolü - SubstituteBindings ← Route model binding ✅ Middleware sırası doğru ✅ InitializeTenancy FIRST (tenant context en başta) ✅ StartSession auto çalışıyor (Laravel 11)
Sorun:
Redis'te session key'ler prefix'siz. Teorik olarak iki tenant aynı session ID üretebilir. Daha önemlisi, tenant context switch sırasında session ID collision riski var.
Kanıt:
Redis keys: fBt6Wv..., N5nNqy..., Zmydda...
↑ Tenant bilgisi YOK!
Etki:
Düşük ihtimal ama mümkün: Session çakışması → Kullanıcı başka tenant'ın session'ına erişir. Daha olası: Session kaybolması (key collision olmadan bile) çünkü tenant context switch sırasında yanlış session ID kullanılabilir.
Sorun:
10+ farklı yerde session regeneration yapılıyor. Her regeneration eski session'ı siliyor. Eğer kullanıcı birden fazla sekme açıksa, bir sekmede regenerate olunca diğer sekmeler 419 alıyor.
Kanıt:
Login → regenerate() Logout → regenerate() CheckApproval → regenerateToken() (her sayfa değişiminde!) CsrfController → regenerateToken() (manuel refresh) ProfileController → regenerateToken()
Etki:
Kullanıcı A: Tab 1'de form açık
Kullanıcı A: Tab 2'de logout yapıyor → session regenerate
Kullanıcı A: Tab 1'de form submit → 419 hatası!
Sorun:
Cookie domain runtime'da set ediliyor (VerifyCsrfToken middleware). Eğer request host'u değişirse (nginx proxy, load balancer vb.), cookie domain mismatch olabilir.
Kanıt:
$config['domain'] = '.' . $request->getHost();
↑ Runtime'da belirleniyor
Etki:
Nadiren: Proxy/load balancer header'ları yanlışsa cookie domain yanlış set edilir → Tarayıcı cookie göndermiyor → Session yok → 419 hatası.
Sorun:
Session için dedicated Redis connection tanımlanmamış. Default connection (DB 0) kullanılıyor. Eğer Redis geçici kesilirse, graceful handling yok.
Kanıt:
config/session.php:
'connection' => null ❌ (default kullan)
config/database.php:
'redis' => [
'default' => ['database' => 0], ← Session burda
'session' => YOK! ❌
]
Etki:
Redis RedisHealthCheckMiddleware var ama session için özel handling yok. Eğer Redis kesilirse → Session kaybolur → Kullanıcı logout olur (auto).
Sorun:
Database setting: 525600 dakika (1 yıl)
Redis avg TTL: 511421032 saniye (~16 yıl!)
→ Neden fark var?
Kanıt:
redis-cli INFO keyspace: db0:keys=798,expires=782,avg_ttl=511421032 525600 dakika = 31536000 saniye (1 yıl) 511421032 saniye = 16.2 yıl (?!)
Etki:
Muhtemelen eski session'lar farklı TTL ile set edilmiş. Ortalamayı yükseltiyor ama yeni session'lar doğru TTL alıyor olmalı. Önemli bir sorun değil ama izlenmeli.
Her tenant için unique Redis prefix ekle. Session key'ler tenant ID ile başlasın.
Kod Değişikliği:
Dosya: app/Providers/TenancyServiceProvider.php
Events\TenancyInitialized::class => [
function (Events\TenancyInitialized $event) {
$tenantId = tenant('id');
// 🔐 Session lifetime
$sessionLifetime = (int) setting('auth_session_lifetime', 525600);
config(['session.lifetime' => $sessionLifetime]);
// 🆔 TENANT-AWARE SESSION PREFIX
$sessionPrefix = 'tenant_' . $tenantId . '_session_';
config(['database.redis.options.prefix' => $sessionPrefix]);
// 🔄 Session store'u yeniden yükle (prefix değişikliğini uygula)
app()->forgetInstance('session.store');
\Log::info('🔐 Tenant session initialized', [
'tenant_id' => $tenantId,
'session_prefix' => $sessionPrefix,
'lifetime' => $sessionLifetime,
]);
},
],
Artıları:
tenant_2_session_ABC123, tenant_1001_session_XYZ789Riskler:
Test Planı:
redis-cli --scan --pattern "tenant_*"
Gereksiz regenerate() ve regenerateToken() çağrılarını kaldır.
Değişiklikler:
1. CheckApproval Middleware:
// ❌ ÖNCEKİ
if (!auth()->user()->is_approved) {
auth()->logout();
$request->session()->invalidate();
$request->session()->regenerateToken(); ← KALDIR!
return redirect()->route('approval.pending');
}
// ✅ YENİ
if (!auth()->user()->is_approved) {
// Session invalidate yeterli, token yenilemeye gerek yok
// Zaten logout oluyor, session zaten silinecek
auth()->logout();
$request->session()->invalidate();
return redirect()->route('approval.pending');
}
2. ProfileController:
// ❌ ÖNCEKİ
public function update(Request $request)
{
// Profile güncelleme
$request->session()->regenerateToken(); ← KALDIR! (Gereksiz)
return back();
}
// ✅ YENİ
public function update(Request $request)
{
// Profile güncelleme
// Token yenilemeye gerek yok (güvenlik riski yok)
return back();
}
3. CsrfController (SAKLA!):
// ✅ CsrfController::refresh() SAKLA
// Manuel CSRF refresh gerekebilir (long-running forms, AJAX retry vs.)
public function refresh(Request $request)
{
$request->session()->regenerateToken();
return csrf_token();
}
Artıları:
Dikkat:
regenerate() SAKLANMALI (güvenlik için gerekli)Session için özel Redis connection tanımla.
Kod Değişikliği:
Dosya: config/database.php
'redis' => [
'default' => [
'database' => 0,
// ... existing config
],
// ✅ YENİ: Dedicated session connection
'session' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => 0, // Session için DB 0 (mevcut)
'read_write_timeout' => 60,
'retry_after' => 100,
'retry_attempts' => 3,
'options' => [
'prefix' => '', // Prefix TenancyServiceProvider'da set edilecek
],
],
'cache' => [
'database' => 1,
// ...
],
];
Dosya: config/session.php
return [
'driver' => 'redis',
'connection' => 'session', ← Dedicated connection kullan
// ...
];
Artıları:
Cookie domain'i config'den al, runtime set etme.
Kod Değişikliği:
Dosya: app/Providers/TenancyServiceProvider.php
Events\TenancyInitialized::class => [
function (Events\TenancyInitialized $event) {
$tenantId = tenant('id');
$primaryDomain = tenant('domains')->where('is_primary', true)->first();
// 🍪 Session cookie domain set et
$cookieDomain = '.' . $primaryDomain->domain;
config(['session.domain' => $cookieDomain]);
\Log::info('🍪 Session cookie domain set', [
'tenant_id' => $tenantId,
'domain' => $cookieDomain,
]);
},
],
Dosya: app/Http/Middleware/VerifyCsrfToken.php (KALDIR)
// ❌ KALDIR - Artık gerekli değil
protected function addCookieToResponse($request, $response)
{
$config = config('session');
// KALDIR: Runtime domain set
// if (tenant()) {
// $host = $request->getHost();
// $config['domain'] = '.' . $host;
// }
// Config'den al (TenancyServiceProvider'da set edilmiş)
$response->headers->setCookie(new Cookie(...));
}
Artıları:
Session sorunlarını proaktif tespit et.
Yeni Middleware:
// app/Http/Middleware/SessionHealthCheck.php
public function handle($request, $next)
{
// Session var mı kontrol et
if (!$request->hasSession()) {
\Log::error('🚨 Session missing!', [
'url' => $request->fullUrl(),
'user_agent' => $request->userAgent(),
'ip' => $request->ip(),
]);
// Telegram alert gönder
\Telegram::sendError('Session missing: ' . $request->fullUrl());
}
// CSRF token var mı kontrol et
$sessionToken = $request->session()->token();
if (!$sessionToken) {
\Log::warning('⚠️ CSRF token missing in session', [
'session_id' => $request->session()->getId(),
'url' => $request->fullUrl(),
]);
}
return $next($request);
}
Artıları:
php artisan config:clear && opcache reset
Metrikler (1 hafta sonra):
• 419 hata sayısı: 100+ → <10 (günlük)
• Session kaybolma: %5 → %0.1
• Kullanıcı şikayeti: Günde 5-10 → 0-1
• Session stability: %70 → %99+