Muzibu Session Bug Analizi

Yanlış "Başka Cihazdan Giriş" Uyarısı - Root Cause Analysis

Tarih
19 Aralık 2025

📝 Basit Anlatım (Sorun Nedir?)

Kullanıcı muzibu.com'de TEK CİHAZDAN müzik dinlerken, sistemden "Başka bir cihazdan giriş yapıldı. Bu oturum sonlandırıldı." uyarısı alıyor ve oturumu kapanıyor.

UYARI YANLIŞ! Gerçekte başka cihazdan giriş yapılmamış. Sistem kullanıcının kendi oturumunu yanlışlıkla "saldırgan oturum" olarak algılayıp sonlandırıyor.

🎯
Neden Önemli?

Kullanıcı deneyimi çok kötü! Müzik dinlerken sürekli atılmak → Platformdan soğuma → Rakip platformlara geçiş (Spotify, Apple Music).

Sistem Nasıl Çalışıyor? (Concurrent Session Management)

🔄 LIFO Mekanizması (Last In, First Out)

Sistem, aynı kullanıcının birden fazla cihazdan giriş yapmasını engellemek için LIFO (Son Giren İlk, Öncekiler Çıkar) yaklaşımı kullanıyor.

Cihaz A login → Session A kaydedilir
Cihaz B login → Session B kaydedilir, Session A SİLİNİR
🔁 Cihaz A polling → Session A DB'de YOK → LOGOUT

🔑 Login Token Cookie Mekanizması

Livewire her sayfa geçişinde session()->getId() değiştirebiliyor (session regeneration). Bu yüzden sistem login token cookie kullanıyor.

✅ Login Token Yaklaşımı
  • • Cookie: mzb_login_token
  • • Süre: 7 gün
  • • Session ID değişse bile token sabit
  • • DB kontrolü: login_token eşleşmesi
❌ Eski Session ID Yaklaşımı
  • • Livewire regenerate → ID değişir
  • • DB'de eski ID aranır
  • • Bulunamaz → False positive logout
  • • Kullanıcı atılır

📡 Frontend Polling Sistemi

Frontend her 5 saniyede bir backend'e istek atarak oturumun geçerliliğini kontrol ediyor.

📄 public/themes/muzibu/js/player/features/session.js:34
const SESSION_POLL_INTERVAL = 5000; // 🧪 TEST: 5 saniye

setInterval(() => {
    this.checkSessionValidity(); // → /api/auth/check-session
}, SESSION_POLL_INTERVAL);
⚠️
Dikkat: 5 saniye çok agresif! Üretim ortamında 300000ms (5 dakika) kullanılmalı.

🔍 Muhtemel Nedenler (6 Senaryo)

1

🍪 Login Token Cookie Silinmesi/Kaybolması

mzb_login_token cookie'si browser tarafından silinmiş veya SameSite/Secure politikaları nedeniyle gönderilmemiş olabilir.

📄 DeviceService.php:82 (Cookie ayarları)
cookie($cookieName, $loginToken, $cookieMinutes, '/', null,
       true,  // secure (HTTPS zorunlu)
       true,  // httpOnly (JS erişemez)
       false, // raw
       'Lax' // SameSite=Lax
);
Olası Sebepler:
  • Browser gizli mod → Cookie silinir
  • Cookie temizleme eklentisi (Privacy Badger, uBlock vb.)
  • Cross-site isteklerde SameSite=Lax bloğu (iframe, 3rd party embed)
  • HTTPS olmayan sayfadan istek (secure=true gereksinimi)
2

⚡ Race Condition: registerSession vs checkSession

Login sonrası registerSession() DB'ye kayıt yaparken, polling sistemi aynı anda checkSession() çağırıyor.

✅ Backend Sıralaması
1. Login POST → registerSession()
2. DB INSERT (50-100ms)
3. Cookie SET
4. Response 200 OK
❌ Frontend Timing Problemi
1. Login SUCCESS alır
2. 2 saniye sonra polling başlar
3. DB INSERT henüz tamamlanmamışsa?
4. sessionExists() → FALSE → LOGOUT
📄 session.js:24-28 (İlk polling delay)
// 🔧 LOGIN SONRASI: Session DB'ye kaydedilmesi için 2 saniye bekle
setTimeout(() => {
    this.checkSessionValidity();
}, 2000); // Yeterli mi? Yavaş network'te değil!
3

🔄 Livewire Session ID Regeneration

Livewire bazı sayfa geçişlerinde session()->regenerate() çağırıyor. Bu durumda session ID değişiyor ama login_token aynı kalıyor.

📄 DeviceService.php:214-222 (Session ID sync)
if ($session) {
    // Token eşleşti - session geçerli
    // Session ID değiştiyse güncelle (Livewire regenerate)
    if ($currentSessionId && $session->session_id !== $currentSessionId) {
        DB::table($this->table)
            ->where('id', $session->id)
            ->update(['session_id' => $currentSessionId]);
    }
    return true;
}
Potansiyel Sorun: getCurrentSessionId() boş dönerse ne olur?
  • API context'te session()->getId() bazen boş döner
  • Cookie decrypt hatası → Session ID alınamaz
  • DB update yapılamaz → Eski session ID kalır → Uyumsuzluk
4

🔁 Aynı Sekmede Çift Login İsteği

Kullanıcı yanlışlıkla login butonuna iki kez tıklarsa veya browser back/forward ile login sayfasına dönüp tekrar login yaparsa, iki ayrı registerSession() çağrısı tetiklenir.

Senaryo: Çift Login
1. Login #1 → Session A (token_A) INSERT
2. Login #2 → Session B (token_B) INSERT
3. LIFO: Session A SİLİNİR (eski session)
4. Browser cookie: token_A (henüz token_B set edilmedi)
5. Polling → token_A DB'de YOK → LOGOUT
Kritik: DeviceService.php:61 satırında DELETE WHERE session_id var ama aynı session_id ile iki login olursa? İlk login'in token'ı silinir!
5

💾 Browser Storage/Cache Senkronizasyon

Kullanıcı iki sekme açtıysa (Tab A, Tab B), her ikisi de aynı login_token cookie'sini paylaşır. Ancak localStorage/sessionStorage senkronize olmaz.

Tab A
• Cookie: token_A
• localStorage: player state, queue
• Polling active
Tab B
• Cookie: token_A (aynı)
• localStorage: aynı veriler
• Polling active (çakışma!)
Sorun: İki sekme aynı anda polling yapıyor. Biri logout olursa clearAllBrowserStorage() çağrılıyor ama diğer sekme etkilenmiyor → Sonsuz döngü riski.
6

💥 Database Hata/Rollback Durumu

registerSession() sırasında DB hatası (connection timeout, deadlock, constraint error) olursa, INSERT başarısız olur ama cookie set edilir.

📄 DeviceService.php:64-77 (Insert işlemi)
DB::table($this->table)->insert([
    'user_id' => $user->id,
    'session_id' => $sessionId,
    'login_token' => $loginToken,
    // ... diğer alanlar
]);

// Cookie SET (INSERT'ten sonra, başarı kontrolü YOK!)
cookie()->queue(cookie($cookieName, $loginToken, ...));
Problem:
  • DB INSERT başarısız → Session DB'de yok
  • Cookie set edilir → Browser'da mzb_login_token var
  • Polling → sessionExists() → DB'de token yok → FALSE
  • Sonuç: Anında logout

🧪 Test Adımları (Problemi Yakalamak İçin)

Test 1

Browser DevTools: Cookie Kontrolü

  1. 1.
    Login yap, F12 aç → Application sekmesi → Cookies
  2. 2.
    mzb_login_token cookie'sinin değerini kopyala (64 char hex)
  3. 3.
    Müzik dinlerken 5 saniye aralıklarla cookie'yi kontrol et → Silinirse Neden 1 ONAYLANDI
  4. 4.
    Console'da document.cookie çalıştır → mzb_login_token görünüyor mu?
Test 2

Database: user_active_sessions Tablosu

mysql -u tuufi.com_ -p tenant_muzibu_1528d0 -e "
SELECT
    id, user_id,
    SUBSTRING(login_token, 1, 16) as token,
    ip_address, device, browser,
    created_at, last_activity
FROM user_active_sessions
WHERE user_id = [KULLANICI_ID]
ORDER BY last_activity DESC;"
  • Kaç session var? 1'den fazlaysa LIFO tetiklenmiş olabilir
  • login_token eşleşiyor mu? Cookie ile DB tokenini karşılaştır
  • last_activity güncel mi? 5 saniyede bir güncellenmeli
Test 3

Laravel Log: Session Termination Kayıtları

tail -f storage/logs/session-terminations-$(date +%Y-%m-%d).log

Oturum kapanma anında bu log dosyasına session termination kaydı yazılacak. İçinde:

  • reason: lifo, manual, timeout, logout, admin
  • user_email: Kullanıcı maili
  • kicked_session: Kapatılan oturumun IP, cihaz, browser bilgisi
  • new_session: Yeni oturumun (kick eden) bilgisi
Dikkat: Eğer reason: 'lifo' görüyorsan ama gerçekte tek cihazdan girişyaptıysan → Neden 4 ONAYLANDI (çift login isteği)
Test 4

Network Tab: API Requests

  1. 1.
    F12Network sekmesi → Preserve log aktif et
  2. 2.
    /api/auth/check-session isteklerini filtrele
  3. 3.
    Her 5 saniyede bir istek görmelisin. Response:
    {"valid": true, "user_id": 22} → OK
    {"valid": false, "reason": "session_terminated"} → PROBLEM
  4. 4.
    Headers sekmesi → Request Cookiesmzb_login_token gönderiliyor mu?
Test 5

Timing Analysis: Race Condition Testi

Login butonuna hızlıca iki kez tıkla (double click) → İki registerSession() çağrısı tetiklenir.

Beklenen Sonuç:
1. İki session INSERT → Session A, Session B
2. LIFO: Session A silinir (eski)
3. Cookie: token_A (ilk set edilen)
4. Polling → token_A DB'de YOK → LOGOUT
Eğer bu test ile logout oluyorsan: Neden 4 kesinleşti. Login butonuna debounce ekle veya registerSession() içinde duplicate check yap.

✅ Önerilen Çözümler

1

📊 Enhanced Logging (Acil - Hemen Uygulanmalı)

DeviceService.php içindeki kritik noktalara detaylı log ekle:

📄 DeviceService.php → registerSession()
\Log::info('🔐 registerSession: START', [
    'user_id' => $user->id,
    'session_id' => substr($sessionId, 0, 16) . '...',
    'login_token' => substr($loginToken, 0, 16) . '...',
]);

// DB INSERT
$inserted = DB::table($this->table)->insert([...]);

if (!$inserted) {
    \Log::error('🔐 registerSession: DB INSERT FAILED', [
        'user_id' => $user->id,
    ]);
    return; // Cookie SET ETME!
}

\Log::info('🔐 registerSession: SUCCESS');
Fayda: DB hataları (Neden 6) hemen tespit edilir.
2

🍪 Cookie Validation & Retry Logic

sessionExists() içinde cookie yoksa HEMEN logout etme, önce retry dene.

📄 DeviceService.php → sessionExists()
$cookieToken = request()->cookie('mzb_login_token');

if (!$cookieToken) {
    // RETRY: 2 saniye sonra tekrar kontrol et
    static $retryCount = 0;
    if ($retryCount < 3) {
        $retryCount++;
        \Log::warning('🔐 sessionExists: Cookie missing, retry #' . $retryCount);
        sleep(2);
        return $this->sessionExists($user); // Recursive retry
    }

    \Log::error('🔐 sessionExists: Cookie missing after 3 retries');
    return false; // Gerçekten yok
}
Fayda: Geçici cookie sync sorunları (browser lag) tolere edilir.
3

🚫 Duplicate Login Request Prevention

Aynı kullanıcının 5 saniye içinde iki kez login yapmasını engelle.

📄 DeviceService.php → registerSession()
// Aynı kullanıcı son 5 saniyede login yaptı mı?
$recentLogin = DB::table($this->table)
    ->where('user_id', $user->id)
    ->where('created_at', '>=', now()->subSeconds(5))
    ->exists();

if ($recentLogin) {
    \Log::warning('🔐 registerSession: Duplicate login attempt (throttled)', [
        'user_id' => $user->id,
    ]);
    return; // Yeni session AÇMA, mevcut geçerli
}
Fayda: Çift tıklama, browser refresh vb. durumlar engellenir (Neden 4).
4

⏱️ Login Sonrası İlk Polling Delay Artırma

Login sonrası ilk polling 2 saniye → 5 saniye arttırılmalı.

📄 session.js → startSessionPolling()
// ❌ ESKİ: 2 saniye (yavaş network'te yetersiz)
setTimeout(() => {
    this.checkSessionValidity();
}, 2000);

// ✅ YENİ: 5 saniye (DB INSERT kesin tamamlanır)
setTimeout(() => {
    this.checkSessionValidity();
}, 5000);
Fayda: Race condition riski azalır (Neden 2).
5

🔐 Cookie SameSite Policy Ayarı

Eğer site iframe içinde veya subdomain'lerden erişiliyorsa, SameSite=None; Secure kullan.

📄 DeviceService.php:82 (Cookie ayarları)
// ❌ ESKİ: SameSite=Lax (iframe'de engellenir)
cookie($cookieName, $loginToken, $cookieMinutes, '/', null, true, true, false, 'Lax');

// ✅ YENİ: SameSite=None (tüm context'lerde çalışır, Secure=true zorunlu)
cookie($cookieName, $loginToken, $cookieMinutes, '/', null, true, true, false, 'None');
Dikkat: SameSite=None kullanırsan HTTPS zorunlu (secure=true). HTTP'de çalışmaz!
6

💾 DB Transaction & Error Handling

registerSession() içinde DB transaction kullan, hata varsa cookie SET ETME.

📄 DeviceService.php → registerSession()
try {
    DB::beginTransaction();

    // Önce bu session'ı sil (varsa)
    DB::table($this->table)->where('session_id', $sessionId)->delete();

    // Yeni session kaydet
    DB::table($this->table)->insert([...]);

    // LIFO enforcement
    $this->enforceDeviceLimit($user, $sessionId);

    DB::commit();

    // ✅ BAŞARILI - Cookie set et
    cookie()->queue(cookie($cookieName, $loginToken, ...));

} catch (\Exception $e) {
    DB::rollBack();

    \Log::error('🔐 registerSession: TRANSACTION FAILED', [
        'user_id' => $user->id,
        'error' => $e->getMessage(),
    ]);

    // ❌ Cookie SET ETME!
    return;
}
Fayda: DB hatası durumunda cookie set edilmez (Neden 6 engellenmiş olur).

🎯 Uygulama Öncelik Sırası

1
🚨 ACIL: Enhanced Logging (Çözüm 1)
Önce problemi tam olarak tespit etmek için logları ekle. Sonraki adımlar buna göre belirlenecek.
2
⚡ YÜKSEK: DB Transaction (Çözüm 6)
DB hatalarında cookie set etme. Kritik güvenlik ve tutarlılık sorunu.
3
🔧 ORTA: Duplicate Login Prevention (Çözüm 3)
Çift tıklama, browser back/forward sorunlarını engeller. Kullanıcı deneyimi iyileşir.
4
⏱️ ORTA: Polling Delay Artırma (Çözüm 4)
Login sonrası race condition riskini azaltır. 2 saniye → 5 saniye.
5
🍪 DÜŞÜK: Cookie Retry Logic (Çözüm 2)
Cookie yoksa hemen logout etme, 3 kez retry dene. Geçici sorunları tolere eder.
6
🔐 ÖZELLİK: Cookie SameSite=None (Çözüm 5)
Sadece iframe veya subdomain kullanılıyorsa gerekli. Normal kullanımda SameSite=Lax yeterli.