OfficeFighters’da oturum yönetimi giriş formundan daha geniş bir problem. Bir oyuncu birden fazla sekme açabilir, bağlantısı kopabilir ve birkaç HTTP isteği aynı anda erişim token’ının süresinin dolduğunu fark edebilir. SessionService ile Angular AuthService bu yarışları birlikte ele alıyor. Bu yazı kaynakta görülen saklama, rotasyon ve iptal kurallarını takip ediyor.
İki token’ın saklama politikası farklı
Angular TokenStore erişim token’ını signal içinde bellekte tutuyor. Sayfa yeniden açıldığında bu değer kalıcı depodan okunmuyor; oturumu sürdürme işi yenileme akışına ait. Refresh token ise __Secure-of_rt çereziyle taşınıyor. RefreshCookie yardımcısındaki ayarlar aşağıdaki gibi.
// RefreshCookie.Write: policy from the application.
response.Cookies.Append("__Secure-of_rt", token, new CookieOptions {
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Path = "/api/auth",
MaxAge = maxAge,
IsEssential = true
});HttpOnly tarayıcı JavaScript’inin çerez değerini okumasını engelliyor; Secure ile HTTPS taşınması, Path ile de ilgili kimlik uçlarına gönderilmesi isteniyor. Bu ayarlar kötü niyetli bir script’in uygulama adına istek yapabilmesini tek başına ortadan kaldırmaz. XSS savunması ve yetkilendirme kontrolleri hâlâ aynı güvenlik sınırının parçası.
Çerezli isteğin ayrıca bir sözleşmesi var
Refresh ve logout gibi çerez kullanan uçlar RequireCsrfHeader ile X-Requested-With: of başlığını bekliyor. SameSite=Strict ve CORS politikası bu kontrolle birlikte düşünülüyor. Başlığın değeri bir kullanıcı sırrı değil; farklı kökenden sıradan bir form gönderimini uygulamanın beklediği istekten ayıran tarayıcı politikalarına dayanıyor. Sunucuda keyfi kökenlere bu başlıkla credential erişimi açmak bu tasarımı zayıflatır.
Refresh token veritabanında özetle aranıyor
SessionService gelen değerin özetini hesaplayıp RefreshTokens satırını buluyor. Süresi dolmuş, aile mutlak süresi bitmiş veya iptal edilmiş kayıt reddediliyor. Kullanıcı hâlâ etkin mi ve aktif yasak var mı soruları da yeni erişim token’ı üretilmeden kontrol ediliyor. Böylece eski bir oturum, hesabın güncel durumundan bağımsız bir erişim kaynağı olmuyor.
Tek kullanım koşullu güncellemeyle korunuyor
Yenileme sırasında UsedAt alanı yalnız kullanılmamış ve iptal edilmemiş satır için dolduruluyor. İki istek aynı satırı okusa da normal sahiplenme tek bir UPDATE sonucuyla belirleniyor.
// SessionService.RefreshAsync: claim the single-use token.
var claimed = row.UsedAt is null && await db.RefreshTokens
.Where(t => t.Id == row.Id && t.UsedAt == null && t.RevokedAt == null)
.ExecuteUpdateAsync(s => s.SetProperty(t => t.UsedAt, now), token) == 1;
// The new token inherits the absolute family expiry.
var (refresh, next) = NewRefresh(
user.Id, row.FamilyId, now, row.FamilyExpiresAt, row.DeviceLabel);Yeni token aynı FamilyId içinde üretiliyor. FamilyExpiresAt eski aileden taşınıyor; her yenileme mutlak oturum ömrünü baştan başlatmıyor. ReplacedById asıl halefi gösterebiliyor. Kullanıcının çıkış yapması veya yeniden kullanım tespiti ise aile düzeyinde iptal olarak ele alınıyor.
On saniyelik tolerans sınırlı bir yarış politikası
Kaynakta eşzamanlı yenileme için on saniyelik ReuseTolerance var. Kullanılmış token kısa pencerede tekrar geldiğinde aileye bir kardeş token üretilebiliyor; kullanılan token’dan sonraki kardeş sayısı denetleniyor ve tolerans sınırsız tekrara dönüşmüyor. Pencere dışı kullanım ya da kardeş sınırının aşılması aileyi ReuseDetected nedeniyle iptal ediyor.
Bu, uygulamanın bilinçli bir kullanılabilirlik ve yeniden kullanım tespiti dengesi. Kısa pencereyi genel bir token tekrar kullanım hakkı gibi ele almak yanlış olur. Testte pencerenin hemen içi, hemen dışı ve üçüncü yarışan çağrı özellikle önemli.
Serializable işlem iptal ile yenilemeyi birlikte düzenliyor
RefreshAsync ve logout aile iptali AtomicWrite üzerinden Serializable transaction içinde çalışıyor. Yarışan işlemden sonra aile durumu yeniden okunabiliyor. İptal bilgisini yazan hata sonuçlarının da commit edilmesi gerekli; aksi hâlde kullanıcıya reddedildi denirken iptal veritabanında kaybolabilirdi. İşlem sonucunun başarılı olmasıyla güvenlik amaçlı değişikliğin kalıcı olması aynı karar değil.
Angular tarafında tek uçuş, Web Locks ve generation
AuthService sekme içi yenilemeleri ortak akışta topluyor; Web Locks kullanılabildiğinde sekmeler arası yenilemeleri sıraya koyuyor. BroadcastChannel giriş ve çıkış durumlarını bildiriyor. generation sayacı, kullanıcı çıkış yaptıktan sonra tamamlanan eski refresh yanıtının yeni bir oturum başlatmasını engelliyor.
Burada ağ hatasıyla oturumun gerçekten reddedilmesi de ayrılıyor. Geçici servis erişimsizliğini yanlış parola gibi ele almak kullanıcıyı gereksiz yere oturumdan düşürebilir. Anlamlı senaryolar; sayfa yenileme, iki sekmede eşzamanlı refresh, refresh beklerken logout, süresi bitmiş aile ve sunucuda iptal edilmiş oturum. Kontrolün amacı yalnız yeni JWT almak değil, bütün istemcilerin aynı oturum kararını izlemesi.
Kaynaklar / References
Microsoft Learn — SignalR authentication
OfficeFighters serisinin diğer yazıları
- OfficeFighters — Supabase Storage ve S3 ile oyun varlıklarını yönetmek
- OfficeFighters — SignalR ve WebSocket ile gerçek zamanlı oyun iletişimi
- OfficeFighters — Phaser, sabit adımlı simülasyon ve snapshot interpolasyonu
- OfficeFighters — MailKit ve outbox ile dayanıklı e-posta gönderimi
- OfficeFighters — PostgreSQL, EF Core ve DB-first veri modeli