ASP.NET CORE AUTH VE JWT

Üç boyutlu üç segmentli JWT token modeli, header payload signature dilimleri ve imza kilit ikonu

Bir mobil uygulama API çağrısı yaptı; sunucu bu isteğin gerçekten yetkili bir kullanıcıdan geldiğini nereden bilecek? Klasik session-cookie modeli tek sunuculu klasik web uygulamasında iş görüyor, ama yük dengeleyici arkasında çoğalmış bir API'de, dağıtık mikroservislerde veya React/Flutter istemcilerde çatlamaya başlıyor. Her isteğin başka bir sunucu örneğine düşebildiği ortamda "oturum" kavramı kolay taşınmıyor.

JWT (JSON Web Token) tam bu probleme cevap veriyor: sunucuda durum tutmayan, taşınabilir, kriptografik olarak imzalanmış bir kimlik kartı. ASP.NET Core ekosistemi JWT'yi en başından beri birinci sınıf vatandaş olarak destekliyor. Bu yazı, sıfırdan JWT entegrasyonu kuran .NET geliştiricileri için pratik bir rota — kurulumdan token üretimine, doğrulamadan refresh akışına kadar olan tipik yolu somut kodla anlatıyor.

JWT Nedir ve Neden Ortaya Çıktı

JWT, RFC 7519 ile standartlaştırılmış, JSON tabanlı küçük bir token formatıdır. Üç bölümden oluşur ve nokta karakteriyle ayrılır: header.payload.signature. Tamamı Base64Url ile kodlanır, gözle bakıldığında okunamasa da şifrelenmiş değildir — imzalanmıştır. Yani içerik herkese açık, ama içeriği değiştirmek imzayı bozar.

Stateless mimaride bunun değeri büyük. Sunucu, gelen token'ı sadece imza ve geçerlilik süresi açısından kontrol eder; veritabanına gidip "bu oturum var mı?" diye sormaz. Yatay ölçeklenen servislerde, mikroservisler arası çağrılarda, web/mobil/cihaz farklı istemcilerin aynı API'yi kullandığı senaryolarda bu özellik hayat kurtarır. Standardın detayları için jwt.io kaynakları hem teorik hem interaktif olarak izlenebilir.

Token Yapısı: Header, Payload, Signature

Üç bölüm farklı işler yapar. Anlamadan kullanmak, gün gelir saatlerce süren "neden 401 dönüyor" araştırmasına yol açar.

  • Header: Token tipini (JWT) ve imza algoritmasını (örneğin HS256, RS256) belirtir. JSON olarak hazırlanır, Base64Url'e kodlanır.
  • Payload: Claim'lerin (iddiaların) yer aldığı bölüm. Kullanıcı kimliği, rolü, token'ın ne zaman üretildiği (iat), ne zaman dolacağı (exp), kim tarafından üretildiği (iss), kim için üretildiği (aud) gibi bilgiler burada durur.
  • Signature: Header ve payload'un, sunucu tarafında saklanan gizli anahtarla birlikte hash'lenmiş hâli. Token'ın gerçekten o sunucudan çıktığını ve değiştirilmediğini ispatlar.

Önemli bir nüans: payload şifrelenmez, sadece imzalanır. Şifre, kimlik numarası gibi hassas bilgileri JWT payload'ına koymak hatalıdır. Token'ı eline geçiren herkes Base64 çözüp içeriği okuyabilir.

Üç boyutlu client ve server küpleri arasında akan JWT token, kesik eğri hatlı yörünge ve anahtar simgesi

ASP.NET Core'da JWT Kurulumu

ASP.NET Core 8/9 ile JWT kurulumu birkaç adıma indirilmiş durumda. Önce gerekli NuGet paketini projeye ekle:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Sonra Program.cs içinde servisi kaydet. Konfigürasyon değerlerini appsettings.json'dan okumak, anahtarı koda gömmekten her zaman daha güvenli:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        };
    });

builder.Services.AddAuthorization();

Pipeline'da middleware sırası kritik — UseAuthentication() her zaman UseAuthorization()'dan önce çağrılır:

app.UseAuthentication();
app.UseAuthorization();

Bu sıra ters dönerse kimlik bilgisi henüz okunmadan yetki kontrolü yapılır ve her şey 401 dönmeye başlar. Yeni başlayanların en sık kaybolduğu satır budur.

Token Üretimi

Login endpoint'i, kullanıcı bilgilerini veritabanından doğruladıktan sonra bir JWT döndürür. Tipik bir token üretim metodu şuna benzer:

public string GenerateToken(User user)
{
    var claims = new[]
    {
        new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
        new Claim(ClaimTypes.Email, user.Email),
        new Claim(ClaimTypes.Role, user.Role),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
    };

    var key = new SymmetricSecurityKey(
        Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
    var credentials = new SigningCredentials(
        key, SecurityAlgorithms.HmacSha256);

    var token = new JwtSecurityToken(
        issuer: _config["Jwt:Issuer"],
        audience: _config["Jwt:Audience"],
        claims: claims,
        expires: DateTime.UtcNow.AddMinutes(15),
        signingCredentials: credentials);

    return new JwtSecurityTokenHandler().WriteToken(token);
}

Dikkat çeken üç şey var. Birincisi expires değerinin kısa tutulması (15 dakika tipik bir başlangıç). İkincisi jti claim'i — token'a benzersiz bir kimlik verir, sonradan iptal listesinde işe yarar. Üçüncüsü gizli anahtarın ortam değişkeninden veya secret manager'dan okunması — repoya commit edilmemesi gerekir.

Token Doğrulama Mantığı

Üretim tarafı görece kolay; asıl iş doğrulamada. Neyse ki ASP.NET Core middleware bu işin büyük kısmını otomatik halleder. Bir endpoint'i koruyacaksan tek yapman gereken [Authorize] attribute'unu eklemek:

[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile()
{
    var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    return Ok(new { userId });
}

Role'e göre filtreleme istersen [Authorize(Roles = "Admin")] yazman yeterli. Daha karmaşık senaryolarda policy tanımlayıp [Authorize(Policy = "RequireAdmin")] şeklinde kullanılır. Policy yaklaşımı zamanla rol bazlı kontrolden çok daha esnektir; rol değişse bile endpoint kodu sabit kalır.

İstemci, token'ı Authorization: Bearer eyJhbGciOi... başlığıyla gönderir. Middleware bu başlığı yakalar, imzayı doğrular, claim'leri HttpContext.User'a yerleştirir. Sen sadece tüketirsin.

Refresh Token: Kısa Ömürlü Token'ın Yan Etkisi

Token süresini 15 dakika tutmak güvenli ama kullanıcı deneyimi açısından sıkıntılı — her 15 dakikada bir yeniden login olmak kabul edilemez. Çözüm refresh token pattern'idir.

Mantık şöyle işler: login sırasında iki token üretilir. Access token kısa ömürlü (15 dakika), refresh token uzun ömürlü (7-30 gün). Access token süresi dolduğunda istemci refresh token ile yeni bir access token talep eder. Refresh token veritabanında saklanır, gerektiğinde iptal edilebilir. Yani access token stateless kalır, refresh token kontrol noktasıdır.

  1. Kullanıcı login olur, sunucu access + refresh token döner.
  2. İstemci access token ile API'ye istek atar.
  3. Access token expire olunca refresh endpoint'ine refresh token gönderilir.
  4. Sunucu refresh token'ı veritabanından doğrular, yeni bir access + refresh token üretir, eski refresh token'ı iptal eder.
  5. Çıkışta refresh token blacklist'e alınır veya silinir.

Refresh token rotation — yani her yenileme sonrası yeni refresh token verilmesi — güvenlik için kritik. Refresh token bir kez kullanılınca çöpe gider. Aynı refresh token iki kere kullanılırsa olası bir token sızıntısı sinyali kabul edilir ve tüm oturum sonlandırılır.

Üç boyutlu iki token kartı modeli, dairesel ok ile yenileme döngüsü ve süre farkını gösteren saat ikonları

Sık Yapılan Hatalar ve Güvenlik Notları

JWT'nin teorisi sade ama pratikte çok sayıda ayak kayma noktası var. Sahada en çok karşılaşılanlar:

  • Hassas veri payload'da: Şifre, TCKN, kart bilgisi gibi şeyler claim'e konmaz. Payload imzalı ama açık. Sadece kimlik referansı ve rol yeterli.
  • Anahtar yetersiz uzunlukta: HS256 algoritması için 256 bitlik (32 byte) anahtar minimumdur. "secret123" gibi kısa anahtar derhâl brute force edilir.
  • Anahtar repoda: appsettings.json içine konup commit edilen anahtarlar GitHub tarama botları tarafından dakikalar içinde bulunur. User Secrets, Azure Key Vault veya ortam değişkenleri kullan.
  • HTTPS olmadan iletim: Bearer token URL Bar'da görünmez ama HTTP üzerinden açık geçer. JWT mutlaka TLS arkasında taşınır.
  • Lifetime validation kapalı: ValidateLifetime = false yazılırsa süresi dolmuş token sonsuza dek geçerli olur. Test ortamında bile bu satırı bırakma.
  • İptal mekanizması yok: Stateless yapı güzel ama "kullanıcı çıkış yaptı, token hemen geçersiz olsun" senaryosu için bir blacklist veya kısa lifetime + refresh token gerekir. Yoksa çalınan token 24 saat çalışmaya devam eder.

Üretim ortamında bu kontrollerin tümünü uygulamak, "JWT kullanıyoruz, güvendeyiz" cümlesini gerçeğe yaklaştırır. Konsept basit, ayrıntı uzun. Mimarinin tamamını el alıştırmasıyla yürütmek isteyenler için ASP.NET Core eğitimi kimlik doğrulama bölümünü minimal API ve controller tarafıyla birlikte ele alır; daha geniş ölçekte servisler arası kimlik aktarımıyla ilgilenenler ise .NET microservices eğitimine göz atabilir.

JWT bir kimlik doğrulama yöntemi değil, bir token formatıdır. Onu güvenli ve sürdürülebilir kullanmak ekibin disiplinine bağlı: kısa ömürlü access token, rotate edilen refresh token, vault'ta saklanan anahtar, doğru middleware sırası. Bu dört şey doğru oturduğunda ASP.NET Core'un JWT katmanı yıllarca sessizce işini görür.