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.

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.

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);
}
}


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

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
}

Güvenlik Açısından Değerlendirme
Sliding Expiration Güvenlik Riskleri:
- Token Hijacking: Çalınan token süresiz kullanılabilir
- Session Fixation: Oturum sabitleme saldırıları
- Concurrent Usage: Aynı token’ın birden fazla cihazda kullanımı
Fixed Expiration Güvenlik Avantajları:
- Zorla Yeniden Kimlik Doğrulama: Belirli periyotlarda
- Sınırlı Zarar: Token çalınsa bile sınırlı süre
- 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.