Uygulama Rehberi v1

Session 419 Fix

Multi-Tenant Laravel Projelerinde CSRF 419 Kısır Döngüsü Çözümü

14 Şubat 2026 • Muzibu.com

1. Problem Tanımı

Belirti

  • Kullanıcılar rastgele "Oturum Süresi Doldu" (419) hatası alıyor
  • Sayfa yenilemede session kaybı
  • Login sonrası anında logout
  • 419 hatası döngüye giriyor, çıkış yok

Kök Neden

Multi-tenant yapıda çoklu session cookie çakışması:

  • Eski format: laravel_session_1001
  • Yeni format: tenant_1001_session
  • Browser her iki cookie'yi de gönderiyor
  • Laravel yanlış session'dan CSRF token okuyor → Mismatch!

Çözüm Özeti

Eski cookie'leri tespit et → Her response'ta expire et → 419 olursa cleanup endpoint'e yönlendir

2. Dosya Listesi

YENİ app/Http/Middleware/CleanupLegacyCookies.php
GÜNCELLE app/Http/Middleware/SetTenantSessionConfig.php
GÜNCELLE app/Http/Middleware/VerifyCsrfToken.php
YENİ app/Http/Controllers/SessionCleanupController.php
GÜNCELLE bootstrap/app.php
GÜNCELLE resources/views/errors/419.blade.php
GÜNCELLE resources/views/themes/[TEMA]/auth/login.blade.php
EKLE routes/web.php (route ekle)

3. CleanupLegacyCookies.php YENİ DOSYA

Konum: app/Http/Middleware/CleanupLegacyCookies.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Cookie;

/**
 * Legacy Cookie Cleanup Middleware
 *
 * Her response'ta eski/yanlış formattaki session cookie'lerini temizler.
 * Bu, 419 kısır döngüsünü önler.
 */
class CleanupLegacyCookies
{
    /**
     * Temizlenecek eski cookie adları
     * Kendi tenant yapınıza göre düzenleyin!
     */
    protected array $legacyCookies = [
        'laravel_session_1001',  // Eski tenant format
        'laravel_session_1',
        'laravel_session_2',
        'laravel_session',       // Generic Laravel
        'muzibu_session',        // Çok eski format (varsa)
        'PHPSESSID',             // PHP native
    ];

    public function handle(Request $request, Closure $next): Response
    {
        // Önce response'u al
        $response = $next($request);

        // Request'teki cookie'leri kontrol et
        $allCookies = $request->cookies->all();
        $legacyInCookies = array_intersect(array_keys($allCookies), $this->legacyCookies);

        // SetTenantSessionConfig'den gelen bilgiyi de ekle
        $legacyFromAttribute = $request->attributes->get('_legacy_cookies_to_expire', []);
        $legacyInRequest = array_unique(array_merge($legacyFromAttribute, $legacyInCookies));

        // Aktif session cookie adını al
        $activeSessionCookie = config('session.cookie', 'tenant_1001_session');

        // Legacy cookie bulunduysa, response'a expire header'ları ekle
        foreach ($legacyInRequest as $cookieName) {
            // Aktif cookie'yi silme!
            if ($cookieName === $activeSessionCookie) {
                continue;
            }

            // Her domain varyasyonunda expire et - Symfony Cookie ile kesin kontrol
            foreach ($this->getCookieDomains() as $domain) {
                $response->headers->setCookie(
                    new Cookie(
                        $cookieName,           // name
                        '',                    // value (boş)
                        1,                     // expire (1 = geçmiş timestamp = sil)
                        '/',                   // path
                        $domain,               // domain
                        true,                  // secure
                        true,                  // httpOnly
                        false,                 // raw
                        'lax'                  // sameSite
                    )
                );
            }
        }

        return $response;
    }

    /**
     * Cookie domain varyasyonlarını döndür
     * Kendi domain yapınıza göre düzenleyin!
     */
    protected function getCookieDomains(): array
    {
        $domains = [];

        // Mevcut tenant'ın domain'lerini al (multi-tenant için)
        if (function_exists('tenant') && tenant()) {
            try {
                $tenantDomains = tenant()->domains()->pluck('domain')->toArray();

                foreach ($tenantDomains as $domain) {
                    $cleanDomain = preg_replace('/^www\./', '', $domain);
                    $domains[] = '.' . $cleanDomain;      // .example.com
                    $domains[] = $cleanDomain;            // example.com
                    $domains[] = 'www.' . $cleanDomain;   // www.example.com
                }
            } catch (\Exception $e) {
                // Hata olursa fallback
            }
        }

        // Fallback: Request host'undan al
        if (empty($domains)) {
            $host = request()->getHost();
            $cleanHost = preg_replace('/^www\./', '', $host);

            $domains = [
                '.' . $cleanHost,
                $cleanHost,
                'www.' . $cleanHost,
            ];
        }

        return array_unique(array_filter($domains));
    }
}

4. SetTenantSessionConfig.php GÜNCELLE

Konum: app/Http/Middleware/SetTenantSessionConfig.php
Değişiklik: Legacy cookie'leri request attribute olarak sakla

Eklenecek Kod (handle metodu içinde)

Session config ayarlandıktan sonra legacy cookie tespiti:

// Session config güncellendikten SONRA ekle:

// CRITICAL: Legacy cookie'leri request'ten kaldır VE expire et
$legacyCookies = [
    'laravel_session_1001',
    'laravel_session',
    'muzibu_session',
    'laravel_session_1',
    'laravel_session_2',
];

$legacyFound = [];
foreach ($legacyCookies as $legacyCookie) {
    if ($legacyCookie !== $newCookieName && $request->cookies->has($legacyCookie)) {
        $legacyFound[] = $legacyCookie;
        // Request'ten kaldır
        $request->cookies->remove($legacyCookie);
    }
}

if (!empty($legacyFound)) {
    // Legacy cookie'leri request attribute olarak sakla
    // CleanupLegacyCookies middleware bu bilgiyi kullanacak
    $request->attributes->set('_legacy_cookies_to_expire', $legacyFound);

    \Log::warning('Found legacy cookies in request', [
        'legacy' => $legacyFound,
        'active_cookie' => $newCookieName,
    ]);
}

5. VerifyCsrfToken.php GÜNCELLE

Konum: app/Http/Middleware/VerifyCsrfToken.php
Değişiklik: 419 hatası olduğunda cookie flush + session-cleanup exclude

<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    protected $except = [
        'api/*',
        'payment/callback/*',
        'telescope/telescope-api/*',
        'logout',
        // Session Cleanup - 419 kısır döngüsünü kırmak için
        'session-cleanup',
    ];

    /**
     * Handle CSRF token mismatch with cookie flush
     */
    public function handle($request, \Closure $next)
    {
        if (
            $this->isReading($request) ||
            $this->runningUnitTests() ||
            $this->inExceptArray($request) ||
            $this->tokensMatch($request)
        ) {
            return tap($next($request), function ($response) use ($request) {
                if ($this->shouldAddXsrfTokenCookie()) {
                    $this->addCookieToResponse($request, $response);
                }
            });
        }

        // TOKEN MISMATCH - Cookie flush ile response döndür
        \Log::info('CSRF Mismatch - Cookie Flush triggered', [
            'ip' => $request->ip(),
            'url' => $request->fullUrl(),
        ]);

        $response = response()->view('errors.419', [], 419);

        // Tüm olası session cookie'lerini expire et
        // KENDİ DOMAIN'İNİZE GÖRE DÜZENLEYİN!
        $cookiesToExpire = [
            'tenant_1001_session',
            'laravel_session_1001',
            'laravel_session',
            'muzibu_session',
            'XSRF-TOKEN',
        ];

        $domains = ['.example.com', 'www.example.com', 'example.com'];

        foreach ($cookiesToExpire as $cookieName) {
            foreach ($domains as $domain) {
                $response->headers->setCookie(
                    \Illuminate\Support\Facades\Cookie::forget($cookieName, '/', $domain)
                );
            }
        }

        return $response;
    }
}

6. SessionCleanupController.php YENİ DOSYA

Konum: app/Http/Controllers/SessionCleanupController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cookie;
use Illuminate\Support\Facades\Log;

/**
 * Session Cleanup Controller
 *
 * 419 CSRF token mismatch durumunda çağrılır.
 * Tüm eski/çakışan session cookie'lerini temizler.
 */
class SessionCleanupController extends Controller
{
    public function cleanup(Request $request)
    {
        Log::info('Session Cleanup triggered', [
            'ip' => $request->ip(),
            'user_agent' => $request->userAgent(),
        ]);

        // Temizlenecek tüm cookie adları
        // KENDİ YAPISINA GÖRE DÜZENLEYİN!
        $cookiesToExpire = [
            'tenant_1001_session',
            'laravel_session_1001',
            'laravel_session',
            'muzibu_session',
            'XSRF-TOKEN',
            'tenant_1_session',
            'tenant_2_session',
            'laravel_session_1',
            'laravel_session_2',
            'PHPSESSID',
        ];

        // Tüm domain varyasyonları
        // KENDİ DOMAIN'İNİZE GÖRE DÜZENLEYİN!
        $domains = [
            '.example.com',
            'www.example.com',
            'example.com',
            null,
        ];

        $response = redirect('/login?cleaned=1')
            ->with('status', 'Oturumunuz yenilendi. Lütfen tekrar giriş yapın.');

        foreach ($cookiesToExpire as $cookieName) {
            foreach ($domains as $domain) {
                if ($domain) {
                    $response->withCookie(Cookie::forget($cookieName, '/', $domain));
                } else {
                    $response->withCookie(Cookie::forget($cookieName));
                }
            }
        }

        return $response;
    }
}

7. bootstrap/app.php GÜNCELLE

Değişiklik: Middleware kayıtları + Exception handler

withMiddleware içine ekle:

->withMiddleware(function (Middleware $middleware) {

    // 1. CSRF Token Override (Laravel 12 için)
    $middleware->web(replace: [
        \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class
            => \App\Http\Middleware\VerifyCsrfToken::class,
    ]);

    // 2. Session Config - StartSession'dan ÖNCE (GLOBAL)
    $middleware->prepend(\App\Http\Middleware\SetTenantSessionConfig::class);

    // 3. Legacy Cookie Cleanup - Her response'ta (GLOBAL)
    $middleware->append(\App\Http\Middleware\CleanupLegacyCookies::class);

    // ... diğer middleware'ler
})

withExceptions içine ekle:

->withExceptions(function (Exceptions $exceptions) {

    // Cookie flush helper
    $flushSessionCookies = function ($response, $request) {
        $cookiesToExpire = [
            'tenant_1001_session',
            'laravel_session_1001',
            'laravel_session',
            'muzibu_session',
            'XSRF-TOKEN',
        ];

        // KENDİ DOMAIN'İNİZE GÖRE DÜZENLEYİN!
        $domains = ['.example.com', 'www.example.com', 'example.com'];

        foreach ($cookiesToExpire as $cookieName) {
            foreach ($domains as $domain) {
                $response->headers->setCookie(
                    cookie()->forget($cookieName, '/', $domain)
                );
            }
        }

        return $response;
    };

    // TokenMismatchException handler
    $exceptions->renderable(function (\Illuminate\Session\TokenMismatchException $e, $request) use ($flushSessionCookies) {
        $response = response()->view('errors.419', [], 419);
        return $flushSessionCookies($response, $request);
    });

    // HttpException 419 handler
    $exceptions->renderable(function (\Symfony\Component\HttpKernel\Exception\HttpException $e, $request) use ($flushSessionCookies) {
        if ($e->getStatusCode() == 419) {
            $response = response()->view('errors.419', [], 419);
            return $flushSessionCookies($response, $request);
        }
    });
})

8. 419.blade.php GÜNCELLE

Konum: resources/views/errors/419.blade.php
Değişiklik: Auto-redirect to /session-cleanup

<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Oturum Süresi Doldu - 419</title>
    <!-- AUTO CLEANUP: 3 saniye sonra yönlendir -->
    <meta http-equiv="refresh" content="3;url=/session-cleanup">
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
            color: #e2e8f0;
            padding: 1rem;
        }
        .container { max-width: 500px; text-align: center; }
        .icon {
            width: 80px; height: 80px;
            margin: 0 auto 1.5rem;
            background: linear-gradient(135deg, #f59e0b, #d97706);
            border-radius: 50%;
            display: flex; align-items: center; justify-content: center;
            font-size: 2rem;
        }
        h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
        .desc { color: #94a3b8; margin-bottom: 1.5rem; }
        .btn {
            display: inline-block;
            padding: 0.75rem 1.5rem;
            background: linear-gradient(135deg, #f59e0b, #d97706);
            color: white;
            text-decoration: none;
            border-radius: 0.5rem;
            font-weight: 600;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="icon">⏱️</div>
        <h1>Oturum Süresi Doldu</h1>
        <p class="desc">
            <span id="countdown">3</span> saniye içinde otomatik yenilenecek...
        </p>
        <a href="/session-cleanup" class="btn">Hemen Yenile</a>
    </div>
    <script>
        var s = 3;
        setInterval(function() {
            s--;
            document.getElementById('countdown').textContent = s;
        }, 1000);
    </script>
</body>
</html>

9. login.blade.php GÜNCELLE - JS Handler

Değişiklik: Form submit'te 419 yakala ve cleanup'a yönlendir

<!-- Login form'una bu attribute'ları ekle: -->
<form method="POST" action="{{ route('login') }}"
      x-data="{ csrfError: false }"
      @submit.prevent="
          const formData = new FormData($el);
          const alreadyCleaned = window.location.search.includes('cleaned=1');

          fetch($el.action, {
              method: 'POST',
              body: formData,
              credentials: 'same-origin',
              headers: { 'Accept': 'text/html,application/xhtml+xml' }
          })
          .then(response => {
              if (response.status === 419) {
                  // CSRF token mismatch
                  if (alreadyCleaned) {
                      // Cleanup zaten yapıldı (sonsuz döngü önleme)
                      window.location.href = '/login';
                      return;
                  }
                  csrfError = true;
                  window.location.href = '/session-cleanup';
                  return;
              }

              if (response.redirected) {
                  window.location.href = response.url;
                  return;
              }

              return response.text().then(html => {
                  document.open();
                  document.write(html);
                  document.close();
              });
          })
          .catch(error => console.error('Login error:', error));
      ">
    <input type="hidden" name="_token" value="{{ csrf_token() }}">
    <!-- ... form içeriği ... -->
</form>

10. routes/web.php ROUTE EKLE

// SESSION CLEANUP - 419 kısır döngüsünü kırmak için
// Tüm eski session cookie'lerini temizler ve login'e yönlendirir
Route::get('/session-cleanup', [\App\Http\Controllers\SessionCleanupController::class, 'cleanup'])
    ->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class])
    ->name('session.cleanup');

11. Test Prosedürü

curl Test

# Fake legacy cookie ile istek at
curl -I "https://www.example.com/" -H "Cookie: laravel_session_1001=fake123"

# Response'ta şunu görmelisin:
# set-cookie: laravel_session_1001=deleted; expires=Thu, 01 Jan 1970...; Max-Age=0

Browser Test

  1. DevTools > Application > Cookies aç
  2. Elle laravel_session_1001=fake123 cookie'si ekle
  3. Sayfayı yenile (F5)
  4. Response headers'da Set-Cookie: laravel_session_1001=...; Max-Age=0 gör
  5. /login sayfasına git
  6. Giriş yap
  7. F5 ile yenile - session korunmalı

Başarı Kriterleri

  • ✅ Fake cookie response'ta expire ediliyor (Max-Age=0)
  • ✅ 419 hatası ASLA görülmüyor (auto-cleanup)
  • ✅ Login başarılı
  • ✅ F5 sonrası session korunuyor

Önemli Notlar