
Bu içeriğimizde, Asp.NET Core mimarisinde kullanıcıya ait kimlik doğrulama oturumunu (authentication ticket) sunucu tarafında saklamamızı sağlayan ITicketStore arayüzünü ele alıyor olacağız.
ITicketStore Nedir?
Asp.NET Core mimarisinde, özellikle Cookie Authentication kullanırken, kullanıcının tüm kimlik bilgilerinin ve claim’lerinin tarayıcıya gönderilen cookie içerisinde taşımaktansa, bu bilgileri Redis, veritabanı yahut başka bir harici depoda tutmak isteyebiliriz.
ITicketStore, doğrudan kullanıcıları veya parolaları saklamamakta, kullanıcının oturumunu temsil edenAuthenticationTicketnesnesini saklamaktadır. Asp.NET Core ise bu mekanizmayıCookieAuthenticationOptions.SessionStoreüzerinden kullanmaktadır.
Normal Cookie Authentication akışını düşünürsek eğer; kullanıcı giriş yapar, AuthenticationTicket(şifrelenmiş ve imzalanmış kimlik bilgileri, claim’ler ve oturum özellikleri) cookie’ye yazılır ve tarayıcı her istekte cookie’yi gönderir. Bu yöntem çoğu uygulama için gayet yeterli olsa da esasında kullanıcıya ait çok sayıda claim, rol, yetki vs. bilgileri cookie’nin yersiz şekilde büyümesine neden olabilmektedir. Bu da tarayıcıların, proxy’lerin ve sunucuların header boyut sınırları yüzünden request süreçlerinde sorun olarak karşımıza çıkabilmektedir.
İşte böyle bir durumda ITicketStore devreye girer ve akış değişir…
ITicketStore ile süreç şöyle cereyan eder… Kullanıcı giriş yapar, AuthenticationTicket sunucu tarafından Redis’e aktarılır ve tarayıcıya cookie gönderilir. Tabi burada cookie, kullanıcın tüm bilgilerini değil session key’ini taşıyacaktır. Böylece sunucu kullanıcının kim olduğunu hatırlıyor olsa da, tarayıcı sadece onu hatırlayacak key’i tutacaktır. Nihai olarak kullanıcıdan gelen her istek cebinde session key eşliğinde ticket barındırıyor olacaktır. Asp.NET Core mimarisi ise bu session key’i alıp ticket’ı Redis’ten bulacak ve kullanıcıyı doğrulayacaktır.
Bu yaklaşımdaki en büyük avantaj, kullanıcının oturumunu sunucudan silebilme imkanının söz konusu olmasıdır. Session key Redis’ten silinirse, o oturumla gelen sonraki istekler doğrulanmayacaktır…
Hangi Durumlarda Kullanılır?
ITicketStore, özellikle kullanıcı oturumlarının sunucu tarafında yönetilmesini istediğimiz tüm durumlarda tercih edilebilir.
Öncelikle, kullanıcıya ait kimlik bilgileri ve claim’ler cookie içerisinde taşınmak yerine Redis yahut veritabanı gibi bir depoda saklandığı için cookie boyutunun küçülmesini sağlayacaktır
Bunun yanında, kullanıcı oturumlarının sunucu üzerinden kontrol edilmesine ve gerektiğinde geçersiz kılınmasına imkan tanıyacaktır. Örneğin, bir kullanıcının oturumunu Redis’ten silerek sonraki isteklerinde kimlik doğrulamasının başarısız olmasını sağlayabiliriz.
Ayrıca birden fazla uygulama sunucusunun (instance) çalıştığı distributed mimarilerde, ortak bir Redis veya veritabanı kullanarak oturumların tüm sunucular tarafından erişilebilir olmasını da sağlayacaktır.
Tüm bunların aksine ITicketStore‘un, kullanıcı adı ve parola gibi kimlik bilgilerini yöneten bir kullanıcı veritabanı olmadığını ve JET Bearer token doğrulama mekanizmasının da yerine geçmeyeceğini söylemekte fayda görmekteyim. Bu yapı, yalnızca Asp.NET Core’un Cookie Authentication mekanizmasıyla birlikte çalışmaktadır.
Nasıl Kullanılır?
ITicketStore‘un mantığını anlayabilmek için basit bir çalışma üzerinden incelemede bulunmamız yeterli olacaktır.
Bunun için öncelikle ITicketStore implementasyonunun aşağıdaki gibi gerçekleştirilmesi gerekir:
public class RedisTicketStore : ITicketStore
{
// Redis bağlantısı için IDatabase instance'ını tutar.
private readonly IDatabase _database;
// Ticket'ın default geçerlilik süresi 8 saat olarak ayarlanmıştır. Eğer ticket'ın Properties.ExpiresUtc değeri null ise, bu default süre kullanılır.
private readonly TimeSpan _defaultExpiration = TimeSpan.FromHours(8);
public RedisTicketStore(IConnectionMultiplexer connectionMultiplexer)
=> _database = connectionMultiplexer.GetDatabase();
// Verilen key'e ait ticket'ı Redis'ten siler.
public async Task RemoveAsync(string key)
=> await _database.KeyDeleteAsync(key);
// Ticket'ı günceller/yeniler ve ticket'ın bitiş zamanını hesaplar, kalan süreyi bulur. Süre dolmuşsa kaydı siler; dolmamışsa bileti kalan süre kadar bir TTL (yaşam süresi) ile Redis'e yazar.
public async Task RenewAsync(string key, AuthenticationTicket ticket)
{
var serializedTicket = TicketSerializer.Default.Serialize(ticket);
var expiration = ticket.Properties.ExpiresUtc ?? DateTimeOffset.UtcNow.Add(_defaultExpiration);
var remainingTime = expiration - DateTimeOffset.UtcNow;
if (remainingTime <= TimeSpan.Zero)
{
await _database.KeyDeleteAsync(key);
return;
}
await _database.StringSetAsync(key, serializedTicket, remainingTime);
}
// Verilen key ile Redis'ten ticket'ı okur, boşsa null döner, varsa deserialize edip AuthenticationTicket olarak geri verir.
public async Task<AuthenticationTicket?> RetrieveAsync(string key)
{
var value = await _database.StringGetAsync(key);
if (value.IsNullOrEmpty)
return null;
return TicketSerializer.Default.Deserialize(value!);
}
// RandomNumberGenerator ile kriptografik olarak güvenli, rastgele 32 byte'lık bir key üretip, bu key eşliğinde AuthenticationTicket'ı Redis'e kaydeder ve key'i döndürür.
public async Task<string> StoreAsync(AuthenticationTicket ticket)
{
var key = Convert.ToHexString(RandomNumberGenerator.GetBytes(32));
await RenewAsync(key, ticket);
return key;
}
}
Buradaki yapılan çalışmada önemli bir ayrıntı mevcuttur. 38. satırda olduğu gibi AuthenticationTicket nensesini JSON ile serialize etmek yerine Asp.NET Core’un kendi TicketSerializer sınıfı kullanılmaktadır. Böylece ticket içerisindeki authentication properties, claims ve identity bilgileri uygun biçimde saklanabilmektedir.
Şimdi ITicketStore arayüzünü implement ettiğimiz RedisTicketStore sınıfını cookie authentication ile ilişkilendirelim. Bunun için Program.cs dosyasına gelelim ve aşağıdaki yapılandırmaları gerçekleştirelim:
#region Redis Connection
var redisConnectionString = builder.Configuration.GetConnectionString("Redis") ?? "localhost:6379";
builder.Services.AddSingleton<IConnectionMultiplexer>(serviceProvider => ConnectionMultiplexer.Connect(redisConnectionString));
#endregion
builder.Services.AddSingleton<ITicketStore, RedisTicketStore>();
builder.Services.AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>, ConfigureCookieAuthenticationOptions>();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
options.ExpireTimeSpan = TimeSpan.FromHours(8);
options.SlidingExpiration = true;
});
builder.Services.AddAuthorization();
Burada görüldüğü üzere Redis ile birlikte temel cookie yapılandırmaları gerçekleştirilmektedir. Ancak 7. satıra göz atarsanız eğer CookieAuthenticationOptions nesnesi yapılandırılırken, bir yandan da ConfigureCookieAuthenticationOptions sınıfımızı son ayar (yani past-configure) adımı olarak çalıştırmasını istemekteyiz. Böylece AddCookie(options => ...) içindeki ayarların (veyahut başka herhangi bir Configure adımının) SessionStore‘u ezme ihtimali kalmayacaktır. Yani PostConfigure en son çalışacağı için son sözü söyleyecektir.
ConfigureCookieAuthenticationOptions içeriğine gelirsek eğer;
public class ConfigureCookieAuthenticationOptions(ITicketStore ticketStore) : IPostConfigureOptions<CookieAuthenticationOptions>
{
public void PostConfigure(string? name, CookieAuthenticationOptions options)
=> options.SessionStore = ticketStore;
}
Bu sınıf RedisTicketStore‘u (ITicketStore), cookie authentication’ın SessionStore özelliğine bağlamaktadır. Böylece kimlik ticket’ları tarayıcı cookie’sinde değil, Redis sunucusunda saklanacak, cookie sadece session key’i taşıyacaktır.
Artık bu vaziyette yapılandırılmış bir projede aşağıdaki gibi SignInAsync çağrısı, SessionStore olarak yapılandırdığımız RedisTicketStore üzerinden StoreAsync metodunu tetikleyecektir.
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, authProperties);
Ve aşağıdaki gibi Authorize attribute’unun kullanıldığı action’lara her erişim süreçlerinde ise yine RedisTicketStore‘un RetrieveAsync metodunu çağıracaktır.
[Authorize]
public IActionResult Privacy()
{
return View();
}
İşte bu kadar…
Nihai olarak,
ITicketStore, Cookie Authentication mekanizmasını sunucu taraflı bir oturum yönetimine dönüştürerek, hem cookie boyutunu küçültmekte hem de oturumlar üzerinde tam kontrol sağlamaktadır. Haliyle doğru kullanıldığı taktirde ölçeklenebilir ve güvenli kimlik doğrulama mimarilerinin vazgeçilmez parçalarından biri olmaktadır diyebiliriz.
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımızda görüşmek üzere…
İyi çalışmalar…