.NET'te Authentication ve Authorization: JWT ve Identity
# .NET'te Authentication ve Authorization
Güvenlik, her API'nin temel gereksinimidir. Authentication (kimlik dogrulama) ile authorization (yetkilendirme) arasindaki farki net bir sekilde ayirmak, saglam bir guvenlik mimarisinin ilk adimidir. ASP.NET Core, her iki alan icin de olgun bir middleware altyapisi ve policy tabanli kontroller sunar. Bu yazida JWT yapilandirmasi, Identity kurulumu, OAuth2/OIDC entegrasyonu ve production sistemlerde kullandigim guvenlik pratiklerini detayli olarak paylasiyorum.
Authentication Yaklasimlari
JWT Bearer Authentication
JWT (JSON Web Token), API authentication icin en yaygin tercih edilen yontemdir. Sunucu, kullanicinin claim'lerini iceren imzali bir token uretir ve sonraki her istek bu token'i `Authorization` header'inda tasir. Sunucu tarafinda oturum durumu tutulmaz, sticky session gereksinimi ortadan kalkar.
`Program.cs` icinde tipik bir JWT yapilandirmasi:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration[class="code-string">"Jwt:Issuer"],
ValidAudience = builder.Configuration[class="code-string">"Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration[class="code-string">"Jwt:Key"]!)),
ClockSkew = TimeSpan.FromSeconds(class="code-number">30)
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception is SecurityTokenExpiredException)
{
context.Response.Headers.Append(class="code-string">"X-Token-Expired", class="code-string">"true");
}
return Task.CompletedTask;
}
};
});`ClockSkew` degerini her zaman dusuk tutarim. Varsayilan 5 dakikalik tolerans cogu sistem icin gereksiz derecede genistir ve suresi dolmus token'larin kabul edildigi bir pencere olusturur.
Token Uretimi
Token uretimi basit gorunur ama detaylar onemlidir. Tipik olarak kullandigim servis yapisi:
public class TokenService : ITokenService
{
private readonly IConfiguration _config;
public TokenService(IConfiguration config)
{
_config = config;
}
public string GenerateAccessToken(ApplicationUser user, IList<string> roles)
{
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.Id),
new(JwtRegisteredClaimNames.Email, user.Email!),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new(class="code-string">"username", user.UserName!)
};
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config[class="code-string">"Jwt:Key"]!));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _config[class="code-string">"Jwt:Issuer"],
audience: _config[class="code-string">"Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(class="code-number">15),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public RefreshToken GenerateRefreshToken()
{
return new RefreshToken
{
Token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(class="code-number">64)),
ExpiresAt = DateTime.UtcNow.AddDays(class="code-number">7),
CreatedAt = DateTime.UtcNow
};
}
}Production ortaminda guvence altina aldigim API'lerde access token suresini 15 dakika veya daha kisa tutarim ve bunlari sunucu tarafinda saklanan refresh token'larla eslestiririm. Bu yaklasim, bir token ele gecirildiginde hasar penceresini minimuma indirir.
ASP.NET Core Identity
Uygulamaniz kullanici yasam dongusu uzerinde tam kontrole sahipse -- kayit, sifre yonetimi, e-posta dogrulama, iki faktorlu kimlik dogrulama -- ASP.NET Core Identity dogru temeldir. Sifre hashleme, hesap kilitleme ve kullanici deposu soyutlamasi gibi agir isleri ustlenir.
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
class=class="code-string">"code-comment">// Sifre kurallari
options.Password.RequireDigit = true;
options.Password.RequiredLength = class="code-number">12;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
class=class="code-string">"code-comment">// Kilitleme
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(class="code-number">15);
options.Lockout.MaxFailedAccessAttempts = class="code-number">5;
options.Lockout.AllowedForNewUsers = true;
class=class="code-string">"code-comment">// Kullanici
options.User.RequireUniqueEmail = true;
options.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();Refresh Token Akisi
Duzgun bir refresh token akisi vazgecilmezdir. Istemci, suresi dolmus access token ile birlikte gecerli bir refresh token gonderir ve yeni bir cift alir:
[HttpPost(class="code-string">"refresh")]
[AllowAnonymous]
public async Task<IActionResult> Refresh([FromBody] TokenRefreshRequest request)
{
var principal = GetPrincipalFromExpiredToken(request.AccessToken);
if (principal is null)
return Unauthorized(class="code-string">"Gecersiz access token.");
var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier);
var user = await _userManager.FindByIdAsync(userId!);
if (user is null)
return Unauthorized();
var storedToken = await _tokenRepository.GetRefreshTokenAsync(user.Id);
if (storedToken is null ||
storedToken.Token != request.RefreshToken ||
storedToken.ExpiresAt <= DateTime.UtcNow ||
storedToken.IsRevoked)
{
class=class="code-string">"code-comment">// Olasi token yeniden kullanim saldirisi -- tum aileyi iptal et
if (storedToken?.IsRevoked == true)
{
await _tokenRepository.RevokeAllTokensForUserAsync(user.Id);
}
return Unauthorized(class="code-string">"Gecersiz veya suresi dolmus refresh token.");
}
class=class="code-string">"code-comment">// Rotasyon: eskiyi iptal et, yenisini uret
storedToken.IsRevoked = true;
var newRefreshToken = _tokenService.GenerateRefreshToken();
newRefreshToken.UserId = user.Id;
newRefreshToken.ReplacedByToken = newRefreshToken.Token;
await _tokenRepository.SaveRefreshTokenAsync(newRefreshToken);
var roles = await _userManager.GetRolesAsync(user);
var newAccessToken = _tokenService.GenerateAccessToken(user, roles);
return Ok(new TokenResponse
{
AccessToken = newAccessToken,
RefreshToken = newRefreshToken.Token
});
}Buradaki kritik detay token ailesi takibidir. Iptal edilmis bir refresh token tekrar kullanilirsa, tum zinciri iptal ederim. Bu, saldirganin meşru istemcinin zaten rotate ettigi bir token'i ele gecirdigi replay saldirilarini tespit eder.
Authorization Stratejisi
Policy Tabanli Authorization
Kod tabanina dagilmis `[Authorize(Roles = "Admin")]` attribute'leri bakim kabusu haline gelir. Policy tabanli authorization mantigi merkezilestir ve test edilebilir kilar:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(class="code-string">"CanManageProducts", policy =>
policy.RequireRole(class="code-string">"Admin", class="code-string">"ProductManager"));
options.AddPolicy(class="code-string">"CanViewReports", policy =>
policy.RequireClaim(class="code-string">"permission", class="code-string">"reports:read"));
options.AddPolicy(class="code-string">"PremiumUser", policy =>
policy.Requirements.Add(new PremiumSubscriptionRequirement()));
class=class="code-string">"code-comment">// Varsayilan olarak reddet: tum endpoint'ler authentication gerektirir
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});Karmasik is kurallari icin `IAuthorizationHandler` uygulayin:
public class PremiumSubscriptionHandler
: AuthorizationHandler<PremiumSubscriptionRequirement>
{
private readonly ISubscriptionService _subscriptionService;
public PremiumSubscriptionHandler(ISubscriptionService subscriptionService)
{
_subscriptionService = subscriptionService;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PremiumSubscriptionRequirement requirement)
{
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId is null) return;
var isActive = await _subscriptionService
.HasActiveSubscriptionAsync(userId);
if (isActive)
{
context.Succeed(requirement);
}
}
}Ardindan endpoint'lere temiz bir sekilde uygulayin:
app.MapGet(class="code-string">"/api/reports", GetReports)
.RequireAuthorization(class="code-string">"CanViewReports");
app.MapDelete(class="code-string">"/api/products/{id}", DeleteProduct)
.RequireAuthorization(class="code-string">"CanManageProducts");Bu yaklasim controller'lari yalin tutar ve authorization mantigini bagımsız olarak test edilebilir kilar.
OAuth2/OIDC Entegrasyonu
Kurumsal SSO veya sosyal giris icin, OpenID Connect uzerinden harici bir identity provider ile entegrasyon standart yoldur. Azure AD veya Keycloak gibi bir provider ile pratik kurulum:
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = builder.Configuration[class="code-string">"Oidc:Authority"];
options.ClientId = builder.Configuration[class="code-string">"Oidc:ClientId"];
options.ClientSecret = builder.Configuration[class="code-string">"Oidc:ClientSecret"];
options.ResponseType = class="code-string">"code";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add(class="code-string">"openid");
options.Scope.Add(class="code-string">"profile");
options.Scope.Add(class="code-string">"email");
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = class="code-string">"name",
RoleClaimType = class="code-string">"role"
};
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = async context =>
{
class=class="code-string">"code-comment">// Harici kullaniciyi yerel veritabanina senkronize et
var userService = context.HttpContext.RequestServices
.GetRequiredService<IUserSyncService>();
await userService.SyncExternalUserAsync(context.Principal!);
}
};
});Tarayici olmayan API'den API'ye senaryolarda client credentials akisini kullanin:
public class MachineToMachineTokenService
{
private readonly HttpClient _httpClient;
private readonly IConfiguration _config;
public async Task<string> GetAccessTokenAsync()
{
var disco = await _httpClient
.GetDiscoveryDocumentAsync(_config[class="code-string">"Oidc:Authority"]);
var tokenResponse = await _httpClient.RequestClientCredentialsTokenAsync(
new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = _config[class="code-string">"Oidc:ServiceClientId"]!,
ClientSecret = _config[class="code-string">"Oidc:ServiceClientSecret"]!,
Scope = class="code-string">"api.read api.write"
});
if (tokenResponse.IsError)
throw new InvalidOperationException(
$class="code-string">"Token istegi basarisiz: {tokenResponse.Error}");
return tokenResponse.AccessToken!;
}
}Production ortaminda guvence altina aldigim API'lerde, ilk giriste harici identity claim'lerini her zaman yerel bir kullanici kaydina senkronize ederim. Bu, uygulamaya denetim izleri icin yerel bir referans saglar ve identity provider'a tekrar tekrar sorgu yapmayi onler.
Guvenlik Saglamlastirma Kontrol Listesi
Her production dagitimi oncesinde gozden gecirdigim kontroller:
class=class="code-string">"code-comment">// Auth endpointclass="code-string">'lerinde hiz sinirlamasi
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter(class="code-string">"AuthEndpoints", limiter =>
{
limiter.PermitLimit = class="code-number">10;
limiter.Window = TimeSpan.FromMinutes(class="code-number">1);
limiter.QueueLimit = class="code-number">0;
});
});
class=class="code-string">"code-comment">// Auth route'larina uygula
app.MapPost(class="code-string">"/api/auth/login", Login)
.RequireRateLimiting(class="code-string">"AuthEndpoints");
app.MapPost(class="code-string">"/api/auth/refresh", Refresh)
.RequireRateLimiting(class="code-string">"AuthEndpoints");Yaygin Auth Hatalari
Yillar icinde projelerde ayni hatalarin tekrarlandigini gordum. Gercek hasara neden olanlar:
Token'lari localStorage'da saklamak
En yaygin hata budur. Herhangi bir XSS acigi, saldırgana token'a tam erisim verir. Access token'lar bellekte (bir JavaScript degiskeni, kalici depolama degil) yasamalidir. Refresh token'lar JavaScript'in okuyamayacagi httpOnly cookie'lerde bulunmalidir.
Refresh token rotasyonu yapmamak
Bir refresh token 30 gun gecerli ve hic rotate edilmiyorsa, tek bir sizinti saldırgana bir aylik oturum verir. Her kullanimda rotate edin ve token ailesini takip edin, boylece eski bir token'in yeniden kullanimi tum zinciri iptal eder.
Yalnizca imzayi dogrulamak
Gecerli bir imza, gecerli bir token anlamina gelmez. Her zaman `iss`, `aud`, `exp` ve `nbf` claim'lerini dogrulayin. Imzayi kontrol eden ama audience'i gormezden gelen sistemler gordum -- bir servis icin uretilen token'lar baska bir serviste calismaya devam ediyordu.
Hassas veri iceren siskin token'lar
JWT'ler sifrelenmis degil, encode edilmistir. Token'lara e-posta adresleri, telefon numaralari veya sistem mimarisini aciga cikaran dahili ID'ler koymayin. Claim'leri minimal tutun -- bir kullanici ID'si ve rol listesi genellikle yeterlidir. Daha fazla veriye ihtiyaciniz varsa sunucu tarafinda arama yapin.
Servisler arasinda paylasilan simetrik anahtarlar
Birden fazla serviste ayni HMAC anahtarini kullanmak, herhangi bir servisin digerleri icin token uretebilecegi anlamina gelir. Mikroservis mimarisinde asimetrik anahtarlar (RSA veya ECDSA) kullanin. Auth servisi private key'i tutar, tuketici servisler yalnizca public key'e ihtiyac duyar.
class=class="code-string">"code-comment">// Tuketici servisler icin asimetrik anahtar dogrulamasi
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new RsaSecurityKey(rsaPublicKey),
ValidateIssuer = true,
ValidIssuer = class="code-string">"https:class="code-commentclass="code-string">">//auth.mycompany.com",
ValidateAudience = true,
ValidAudience = class="code-string">"https:class="code-commentclass="code-string">">//api.mycompany.com"
};Token iptal stratejisinin olmamasi
JWT'ler tasarim geregi stateless'tir, bu da ek altyapi olmadan onlari iptal edemeyeceginiz anlamina gelir. Production sistemlerde, middleware'in her istekte kontrol ettigi bir iptal listesi (Redis'te kisa omurlu cache) tutarim. Ek yuk, sagladigi guvenlige kiyasla minimaldir.
Auth olaylarini kaydetmemek
"Kim, nereden, ne zaman giris yapti ve neye eristi?" sorularini cevaplayamiyorsaniz production icin hazir degilsiniz. Her authentication ve authorization karari bir denetim olayi uretmelidir.
Sonuc
Dogru authentication stratejisi, API guvenliginin temelidir. .NET'te etkili bir guvenlik mimarisi, standart tabanli authentication'i acik ve policy odakli authorization ile birlestirir. Framework saglam yapi taslari sunar, ancak guvenli bir sistem ile savunmasiz bir sistem arasindaki fark detaylarda gizlidir -- token omru, rotasyon stratejileri, depolama kararlari ve tutarli denetim kaydi.
Guvenlik mimarinizi gozden gecirebilirim.
İlgili Makaleler
.NET Nedir? Modern Backend Geliştirme Rehberi
.NET platformunun ne olduğunu, nasıl çalıştığını ve neden kurumsal projelerde tercih edildiğini öğrenin.
ASP.NET Core ile RESTful API Geliştirme
ASP.NET Core ile production-ready REST API geliştirmenin temellerini öğrenin. Controller, routing ve best practice'ler.
.NET'te Clean Architecture: Ölçeklenebilir Proje Yapısı
.NET projelerinde Clean Architecture uygulayın. Katmanlar, bağımlılık yönetimi ve test edilebilir kod için rehber.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç