JWT Refresh Token Stratejileri: Sliding vs Fixed Expiration

Modern .NET uygulamalarında kullanıcı oturumlarını yönetmek, hem güvenlik hem de kullanıcı deneyimi açısından kritik bir konudur. JWT refresh token stratejileri bu noktada devreye girer ve doğru stratejiyi seçmek ASP.NET Core uygulamanızın güvenliğini doğrudan etkiler.

Bu yazıda JWT refresh token’ların iki ana stratejisini C# kod örnekleriyle derinlemesine inceleyeceğiz: Sliding Expiration ve Fixed Expiration. Her iki yaklaşımın avantajlarını, dezavantajlarını ve hangi senaryolarda tercih edilmesi gerektiğini öğreneceksiniz.

SlidingvsFixedExpiration

ASP.NET Core Identity, Entity Framework Core ve güvenlik uzmanlarının önerdiği en iyi uygulamaları, gerçek C# kod örnekleri ve performans karşılaştırmalarıyla birlikte sunacağız. Yazının sonunda hangi stratejiyi ne zaman kullanmanız gerektiğini net bir şekilde anlayacak ve kendi .NET uygulamanız için doğru kararı verebileceksiniz.

jwt

Bu yazıda:

  • JWT Refresh Token Nedir?
  • Sliding Expiration Stratejisi (C# ile)
  • Fixed Expiration Stratejisi (C# ile)
  • Detaylı Karşılaştırma
  • ASP.NET Core Implementasyonu
  • Entity Framework Core ile Token Yönetimi
  • Güvenlik Açısından Değerlendirme
  • Hangi Stratejiyi Ne Zaman Kullanmalı?
  • Gerçek Dünya Örnekleri
  • Sık Sorulan Sorular

JWT Refresh Token Nedir?

JWT (JSON Web Token) refresh token’lar, access token’ların süresinin dolması durumunda yeni token’lar almak için kullanılan güvenlik mekanizmalarıdır. Access token’lar genellikle kısa süreli (15-30 dakika) olurken, refresh token’lar daha uzun süreli (7-30 gün) olarak tasarlanır.

Refresh token’ların temel amacı:

  • Kullanıcının sürekli giriş yapmasını engellemek
  • Access token’ların kısa süreli olmasını sağlayarak güvenliği artırmak
  • Güvenlik açığı durumunda token’ları hızlıca iptal edebilmek

Sliding Expiration Stratejisi

Sliding expiration (kayar son kullanma) stratejisinde, refresh token her kullanıldığında yeni bir son kullanma tarihi alır. Bu yaklaşım kullanıcı aktivitesine dayalı bir oturum yönetimi sağlar.

Sliding Expiration C# Model Yapısı

// RefreshToken Entity
public class RefreshToken
{
    public int Id { get; set; }
    public string Token { get; set; }
    public string UserId { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime ExpiresAt { get; set; }
    public DateTime? LastUsed { get; set; }
    public bool IsActive { get; set; } = true;
    public string CreatedByIp { get; set; }
    
    // Navigation Properties
    public ApplicationUser User { get; set; }
}

// Token Response DTO
public class TokenResponse
{
    public string AccessToken { get; set; }
    public string RefreshToken { get; set; }
    public DateTime ExpiresAt { get; set; }
}

Sliding Expiration Service Implementasyonu

public interface ITokenService
{
    Task<TokenResponse> GenerateTokensAsync(ApplicationUser user);
    Task<TokenResponse> RefreshTokenAsync(string refreshToken);
    Task RevokeTokenAsync(string refreshToken);
}

public class SlidingTokenService : ITokenService
{
    private readonly IConfiguration _configuration;
    private readonly ApplicationDbContext _context;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly ILogger<SlidingTokenService> _logger;

    // Token yaşam süreleri
    private readonly TimeSpan _accessTokenLifetime = TimeSpan.FromMinutes(15);
    private readonly TimeSpan _refreshTokenLifetime = TimeSpan.FromDays(7);

    public SlidingTokenService(
        IConfiguration configuration,
        ApplicationDbContext context,
        UserManager<ApplicationUser> userManager,
        ILogger<SlidingTokenService> logger)
    {
        _configuration = configuration;
        _context = context;
        _userManager = userManager;
        _logger = logger;
    }

    public async Task<TokenResponse> GenerateTokensAsync(ApplicationUser user)
    {
        var accessToken = GenerateAccessToken(user);
        var refreshToken = await GenerateRefreshTokenAsync(user.Id);

        return new TokenResponse
        {
            AccessToken = accessToken,
            RefreshToken = refreshToken.Token,
            ExpiresAt = DateTime.UtcNow.Add(_accessTokenLifetime)
        };
    }

    public async Task<TokenResponse> RefreshTokenAsync(string refreshTokenValue)
    {
        var refreshToken = await _context.RefreshTokens
            .Include(rt => rt.User)
            .FirstOrDefaultAsync(rt => rt.Token == refreshTokenValue && rt.IsActive);

        if (refreshToken == null || refreshToken.ExpiresAt <= DateTime.UtcNow)
        {
            throw new SecurityTokenException("Invalid refresh token");
        }

        // SLIDING: Eski token'ı geçersiz kıl
        refreshToken.IsActive = false;
        refreshToken.LastUsed = DateTime.UtcNow;

        // YENİ refresh token oluştur (sliding)
        var newRefreshToken = await GenerateRefreshTokenAsync(refreshToken.UserId);
        var newAccessToken = GenerateAccessToken(refreshToken.User);

        await _context.SaveChangesAsync();

        _logger.LogInformation($"Token refreshed for user {refreshToken.UserId}");

        return new TokenResponse
        {
            AccessToken = newAccessToken,
            RefreshToken = newRefreshToken.Token,
            ExpiresAt = DateTime.UtcNow.Add(_accessTokenLifetime)
        };
    }

    private async Task<RefreshToken> GenerateRefreshTokenAsync(string userId)
    {
        var refreshToken = new RefreshToken
        {
            Token = GenerateSecureToken(),
            UserId = userId,
            CreatedAt = DateTime.UtcNow,
            ExpiresAt = DateTime.UtcNow.Add(_refreshTokenLifetime), // Her seferinde yenilenir
            IsActive = true
        };

        _context.RefreshTokens.Add(refreshToken);
        await _context.SaveChangesAsync();

        return refreshToken;
    }

    private string GenerateAccessToken(ApplicationUser user)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(_configuration["JWT:Secret"]);
        
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[]
            {
                new Claim(ClaimTypes.NameIdentifier, user.Id),
                new Claim(ClaimTypes.Email, user.Email),
                new Claim("token_type", "access")
            }),
            Expires = DateTime.UtcNow.Add(_accessTokenLifetime),
            SigningCredentials = new SigningCredentials(
                new SymmetricSecurityKey(key), 
                SecurityAlgorithms.HmacSha256Signature)
        };

        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }

    private string GenerateSecureToken()
    {
        var randomBytes = new byte[64];
        using var rng = RandomNumberGenerator.Create();
        rng.GetBytes(randomBytes);
        return Convert.ToBase64String(randomBytes);
    }
}

jwt

jwt

Fixed Expiration Stratejisi

Fixed expiration (sabit son kullanma) stratejisinde, refresh token’ın son kullanma tarihi oluşturulduğu anda belirlenir ve değişmez. Token ne kadar kullanılırsa kullanılsın, belirlenen tarihte sona erer.

Fixed Expiration Service Implementasyonu

public class FixedTokenService : ITokenService
{
    private readonly IConfiguration _configuration;
    private readonly ApplicationDbContext _context;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly ILogger<FixedTokenService> _logger;

    // Token yaşam süreleri
    private readonly TimeSpan _accessTokenLifetime = TimeSpan.FromMinutes(15);
    private readonly TimeSpan _refreshTokenLifetime = TimeSpan.FromDays(30); // Daha uzun süre

    public FixedTokenService(
        IConfiguration configuration,
        ApplicationDbContext context,
        UserManager<ApplicationUser> userManager,
        ILogger<FixedTokenService> logger)
    {
        _configuration = configuration;
        _context = context;
        _userManager = userManager;
        _logger = logger;
    }

    public async Task<TokenResponse> GenerateTokensAsync(ApplicationUser user)
    {
        var accessToken = GenerateAccessToken(user);
        var refreshToken = await GenerateRefreshTokenAsync(user.Id);

        return new TokenResponse
        {
            AccessToken = accessToken,
            RefreshToken = refreshToken.Token,
            ExpiresAt = DateTime.UtcNow.Add(_accessTokenLifetime)
        };
    }

    public async Task<TokenResponse> RefreshTokenAsync(string refreshTokenValue)
    {
        var refreshToken = await _context.RefreshTokens
            .Include(rt => rt.User)
            .FirstOrDefaultAsync(rt => rt.Token == refreshTokenValue && rt.IsActive);

        if (refreshToken == null || refreshToken.ExpiresAt <= DateTime.UtcNow)
        {
            throw new SecurityTokenException("Invalid refresh token");
        }

        // FIXED: Sadece LastUsed güncelle, token aynı kalır
        refreshToken.LastUsed = DateTime.UtcNow;
        
        // YENİ access token oluştur, refresh token değişmez
        var newAccessToken = GenerateAccessToken(refreshToken.User);

        await _context.SaveChangesAsync();

        _logger.LogInformation($"Access token refreshed for user {refreshToken.UserId}");

        return new TokenResponse
        {
            AccessToken = newAccessToken,
            RefreshToken = refreshTokenValue, // AYNI refresh token
            ExpiresAt = DateTime.UtcNow.Add(_accessTokenLifetime)
        };
    }

    private async Task<RefreshToken> GenerateRefreshTokenAsync(string userId)
    {
        var refreshToken = new RefreshToken
        {
            Token = GenerateRefreshTokenValue(userId), // JWT olarak oluştur
            UserId = userId,
            CreatedAt = DateTime.UtcNow,
            ExpiresAt = DateTime.UtcNow.Add(_refreshTokenLifetime), // SABİT tarih
            IsActive = true
        };

        _context.RefreshTokens.Add(refreshToken);
        await _context.SaveChangesAsync();

        return refreshToken;
    }

    private string GenerateRefreshTokenValue(string userId)
    {
        // Fixed expiration için JWT olarak oluştur
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(_configuration["JWT:RefreshSecret"]);
        
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[]
            {
                new Claim(ClaimTypes.NameIdentifier, userId),
                new Claim("token_type", "refresh")
            }),
            Expires = DateTime.UtcNow.Add(_refreshTokenLifetime), // SABİT süre
            SigningCredentials = new SigningCredentials(
                new SymmetricSecurityKey(key), 
                SecurityAlgorithms.HmacSha256Signature)
        };

        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }
}

Fixed Expiration Avantajları:

  • Daha güvenli: Belirli bir süre sonra zorla yeniden giriş
  • Basit implementasyon
  • Düşük veritabanı yükü
  • Öngörülebilir davranış

Fixed Expiration Dezavantajları:

  • Kullanıcı deneyimi zorluğu
  • Beklenmedik logout durumları
  • Uzun dönem token saklama riski

jwt

Detaylı Karşılaştırma

Özellik Sliding Expiration Fixed Expiration
Güvenlik Seviyesi Orta (aktif kullanımda güvenli) Yüksek (zorla yenileme)
Kullanıcı Deneyimi Mükemmel (kesintisiz) Orta (periyodik giriş)
C# Implementasyon Karmaşık (token rotation) Basit (stateless)
EF Core Performans Yüksek DB yükü Düşük DB yükü
Token Yönetimi Zor (sürekli yeni token) Kolay (tek token)
Ölçeklenebilirlik Orta Yüksek

ASP.NET Core Controller Implementasyonu (H2)

Authentication Controller

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly ITokenService _tokenService;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;

    public AuthController(
        ITokenService tokenService,
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager)
    {
        _tokenService = tokenService;
        _userManager = userManager;
        _signInManager = signInManager;
    }

    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginRequest request)
    {
        var user = await _userManager.FindByEmailAsync(request.Email);
        if (user == null)
            return Unauthorized("Invalid credentials");

        var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
        if (!result.Succeeded)
            return Unauthorized("Invalid credentials");

        var tokenResponse = await _tokenService.GenerateTokensAsync(user);
        
        // Refresh token'ı HTTP-only cookie'de sakla
        var cookieOptions = new CookieOptions
        {
            HttpOnly = true,
            Secure = true,
            SameSite = SameSiteMode.Strict,
            Expires = tokenResponse.ExpiresAt.AddDays(7) // Sliding için dinamik
        };
        
        Response.Cookies.Append("refreshToken", tokenResponse.RefreshToken, cookieOptions);

        return Ok(new { AccessToken = tokenResponse.AccessToken });
    }

    [HttpPost("refresh")]
    public async Task<IActionResult> RefreshToken()
    {
        var refreshToken = Request.Cookies["refreshToken"];
        if (string.IsNullOrEmpty(refreshToken))
            return Unauthorized("Refresh token not found");

        try
        {
            var tokenResponse = await _tokenService.RefreshTokenAsync(refreshToken);
            
            // Yeni refresh token'ı cookie'de güncelle (Sliding için)
            var cookieOptions = new CookieOptions
            {
                HttpOnly = true,
                Secure = true,
                SameSite = SameSiteMode.Strict,
                Expires = DateTime.UtcNow.AddDays(7)
            };
            
            Response.Cookies.Append("refreshToken", tokenResponse.RefreshToken, cookieOptions);

            return Ok(new { AccessToken = tokenResponse.AccessToken });
        }
        catch (SecurityTokenException ex)
        {
            Response.Cookies.Delete("refreshToken");
            return Unauthorized(ex.Message);
        }
    }

    [HttpPost("logout")]
    [Authorize]
    public async Task<IActionResult> Logout()
    {
        var refreshToken = Request.Cookies["refreshToken"];
        if (!string.IsNullOrEmpty(refreshToken))
        {
            await _tokenService.RevokeTokenAsync(refreshToken);
        }

        Response.Cookies.Delete("refreshToken");
        return Ok(new { Message = "Logged out successfully" });
    }
}

Entity Framework Core Konfigürasyonu

DbContext Konfigürasyonu

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) 
        : base(options) { }

    public DbSet<RefreshToken> RefreshTokens { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        // RefreshToken konfigürasyonu
        builder.Entity<RefreshToken>(entity =>
        {
            entity.HasKey(rt => rt.Id);
            
            entity.Property(rt => rt.Token)
                  .IsRequired()
                  .HasMaxLength(500);
            
            entity.Property(rt => rt.UserId)
                  .IsRequired();

            entity.HasIndex(rt => rt.Token)
                  .IsUnique();

            entity.HasIndex(rt => new { rt.UserId, rt.IsActive });

            // User ile ilişki
            entity.HasOne(rt => rt.User)
                  .WithMany()
                  .HasForeignKey(rt => rt.UserId)
                  .OnDelete(DeleteBehavior.Cascade);
        });
    }
}

Startup.cs Konfigürasyonu

public void ConfigureServices(IServiceCollection services)
{
    // Entity Framework
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    // Identity
    services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

    // JWT Authentication
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.ASCII.GetBytes(Configuration["JWT:Secret"])),
                    ValidateIssuer = false,
                    ValidateAudience = false,
                    ValidateLifetime = true,
                    ClockSkew = TimeSpan.Zero
                };
            });

    // Token Service - Stratejiye göre seç
    // services.AddScoped<ITokenService, SlidingTokenService>(); // Sliding için
    services.AddScoped<ITokenService, FixedTokenService>(); // Fixed için
}

jwt

Güvenlik Açısından Değerlendirme

Sliding Expiration Güvenlik Riskleri:

  1. Token Hijacking: Çalınan token süresiz kullanılabilir
  2. Session Fixation: Oturum sabitleme saldırıları
  3. Concurrent Usage: Aynı token’ın birden fazla cihazda kullanımı

Fixed Expiration Güvenlik Avantajları:

  1. Zorla Yeniden Kimlik Doğrulama: Belirli periyotlarda
  2. Sınırlı Zarar: Token çalınsa bile sınırlı süre
  3. Basit Revocation: Token iptal etme kolaylığı

C# ile Güvenlik İyileştirmeleri:

public class SecureTokenService : ITokenService
{
    // Token reuse detection (Sliding için)
    public async Task<bool> DetectTokenReuseAsync(string tokenValue)
    {
        var token = await _context.RefreshTokens
            .FirstOrDefaultAsync(rt => rt.Token == tokenValue);

        return token != null && !token.IsActive; // Pasif token kullanım girişimi
    }

    // Device tracking
    public async Task TrackDeviceAsync(string userId, string deviceInfo, string ipAddress)
    {
        var deviceToken = new DeviceToken
        {
            UserId = userId,
            DeviceInfo = deviceInfo,
            IpAddress = ipAddress,
            LastSeen = DateTime.UtcNow
        };

        _context.DeviceTokens.Add(deviceToken);
        await _context.SaveChangesAsync();
    }

    // Suspicious activity detection
    public async Task<bool> IsSuspiciousActivityAsync(string userId, string ipAddress)
    {
        var recentActivity = await _context.RefreshTokens
            .Where(rt => rt.UserId == userId)
            .Where(rt => rt.CreatedAt > DateTime.UtcNow.AddHours(-1))
            .CountAsync();

        return recentActivity > 5; // 1 saatte 5'ten fazla token
    }
}

Hangi Stratejiyi Ne Zaman Kullanmalı?

Sliding Expiration Kullanın:

  • SaaS uygulamaları için (sürekli kullanım)
  • Mobil uygulamalar için (kesintisiz deneyim)
  • İç kurumsal uygulamalar için (güvenlik riski düşük)
  • Kullanıcı deneyimi kritik uygulamalar için

Fixed Expiration Kullanın:

  • Finansal uygulamalar için (yüksek güvenlik)
  • E-ticaret platformları için (periyodik doğrulama)
  • Kamu kurumu uygulamaları için (compliance)
  • Yüksek riskli veriler barındıran uygulamalar için

Gerçek Dünya C# Örnekleri

Banking Application (Fixed Expiration)

public class BankingTokenService : FixedTokenService
{
    private readonly TimeSpan _refreshTokenLifetime = TimeSpan.FromHours(8); // Kısa süre
    
    protected override async Task<RefreshToken> GenerateRefreshTokenAsync(string userId)
    {
        // Önceki tüm token'ları geçersiz kıl
        var existingTokens = await _context.RefreshTokens
            .Where(rt => rt.UserId == userId && rt.IsActive)
            .ToListAsync();

        foreach (var token in existingTokens)
        {
            token.IsActive = false;
        }

        return await base.GenerateRefreshTokenAsync(userId);
    }
}

Social Media App (Sliding Expiration)

public class SocialMediaTokenService : SlidingTokenService
{
    private readonly TimeSpan _refreshTokenLifetime = TimeSpan.FromDays(90); // Uzun süre
    
    public override async Task<TokenResponse> RefreshTokenAsync(string refreshToken)
    {
        // Kullanıcı aktivitesini log'la
        await LogUserActivityAsync(refreshToken);
        
        return await base.RefreshTokenAsync(refreshToken);
    }

    private async Task LogUserActivityAsync(string refreshToken)
    {
        var token = await _context.RefreshTokens
            .FirstOrDefaultAsync(rt => rt.Token == refreshToken);

        if (token != null)
        {
            var activity = new UserActivity
            {
                UserId = token.UserId,
                ActivityType = "TokenRefresh",
                Timestamp = DateTime.UtcNow
            };

            _context.UserActivities.Add(activity);
            await _context.SaveChangesAsync();
        }
    }
}

Sık Sorulan Sorular

Sliding Expiration’da token süresiz mi uzar?

Hayır, genellikle maksimum yaşam süresi belirlenir. Örneğin 30 gün sonra kullanıcıdan yeniden giriş istenir.

Fixed Expiration’da kullanıcı deneyimi nasıl iyileştirilir?

Kullanıcıya token süresi dolmadan önce uyarı göstererek proaktif yeniden giriş sağlanabilir.

C#’ta hangi JWT kütüphanesi önerilir?

Microsoft’un resmi kütüphanesidir ve ASP.NET Core ile mükemmel entegrasyon sağlar.

Entity Framework Core performansı nasıl optimize edilir?

Index’leme, lazy loading’i kapatma ve query optimization teknikleri kullanılmalıdır.

Refresh token’lar veritabanında şifrelenmeli mi?

Evet, özellikle GDPR uyumluluğu için token değerleri hash’lenmelidir.

By tanju.bozok

Software Architect, Developer, and Entrepreneur

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir