Blog'a Dön
SecurityAuthenticationAzure ADJWT.NET

Kurumsal Uygulamalarda Güvenli Kimlik Doğrulama: Azure AD, JWT ve Session Cookie'leri

Umut Korkmaz2025-10-1011 min

Kurumsal uygulamalarda kimlik doğrulama hiçbir zaman "sadece JWT kullan" kadar basit değildir. Gerçek dünyada birden fazla kimlik sağlayıcıyla, yalnızca cookie anlayan eski sistemlerle, farklı güvenlik gereksinimlerine sahip ağ bölgeleriyle ve her konuda fikri olan uyumluluk ekipleriyle uğraşırsınız. DigiFlow'da, aynı anda üç kimlik doğrulama mekanizmasını destekleyen hibrit bir kimlik doğrulama sistemi inşa ettim ve tam olarak nasıl çalıştığını paylaşmak istiyorum.

Problem

DigiFlow, üç farklı senaryodaki kullanıcılara hizmet veriyor:

  1. İç ağ kullanıcıları — domain'e bağlı Windows makinelerinde, Windows Authentication (Negotiate/NTLM) üzerinden sorunsuz tek oturum açma (SSO) bekleyenler
  2. Dış kullanıcılar ve JWT bearer token'larıyla Azure AD üzerinden kimlik doğrulayan modern istemciler
  3. Hâlâ eski ASP.NET uygulamasından gelen session cookie'lerine dayanan eski entegrasyonlar

Her mekanizmanın farklı güven seviyeleri, farklı token ömürleri ve farklı güvenlik etkileri vardır. Zorluk, bunları tek bir yetkilendirme modelinde birleştirmek; böylece uygulamanın geri kalanı kullanıcının nasıl kimlik doğruladığını umursamasın.

Smart Auth Handler

Çözümün özü, gelen her isteği inceleyen ve uygun kimlik doğrulama mekanizmasına yönlendiren özel bir kimlik doğrulama handler'ıdır:

csharp
public class SmartAuthSchemeOptions : AuthenticationSchemeOptions
{
    public string JwtScheme { get; set; } = "Bearer";
    public string WindowsScheme { get; set; } = "Negotiate";
    public string SessionScheme { get; set; } = "Session";
}

public class SmartAuthHandler : AuthenticationHandler<SmartAuthSchemeOptions>
{
    private readonly ISessionStore _sessionStore;

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        // Öncelik 1: JWT Bearer (en güvenli, tercih edilen)
        var authHeader = Request.Headers.Authorization.FirstOrDefault();
        if (authHeader?.StartsWith("Bearer ") == true)
        {
            var result = await Context.AuthenticateAsync(Options.JwtScheme);
            if (result.Succeeded)
                return EnrichWithClaims(result, "AzureAD");
        }

        // Öncelik 2: Windows Authentication
        var windowsResult = await Context.AuthenticateAsync(Options.WindowsScheme);
        if (windowsResult.Succeeded)
            return await EnrichWindowsIdentity(windowsResult);

        // Öncelik 3: Session cookie (eski)
        if (Request.Cookies.TryGetValue(".DigiFlow.Session", out var sessionId))
        {
            var session = await _sessionStore.GetAsync(sessionId);
            if (session is not null && !session.IsExpired)
                return CreateSessionResult(session);
        }

        return AuthenticateResult.NoResult();
    }
}

Sıra önemlidir. JWT önce kontrol edilir, çünkü en açık şekilde güvenli olandır — istemci, bir bearer token eklemek için bilinçli bir tercih yapmıştır. Windows Auth ikincidir, çünkü örtüktür (tarayıcı kimlik bilgilerini otomatik gönderir). Session cookie'leri ise eski yedek olarak en sondadır.

16 Güvenlik Middleware'i

Kimlik doğrulama yalnızca ilk katmandır. DigiFlow'un, her istekte çalışan 16 güvenlik middleware'i var. İşte en kritik olanları:

csharp
// Program.cs - Middleware pipeline'ı (sıra kritiktir)
app.UseMiddleware<RequestIdMiddleware>();
app.UseMiddleware<SecurityHeadersMiddleware>();
app.UseMiddleware<IpWhitelistMiddleware>();
app.UseMiddleware<RateLimitingMiddleware>();
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseMiddleware<PiiSanitizationMiddleware>();
app.UseAuthentication();
app.UseMiddleware<SessionValidationMiddleware>();
app.UseMiddleware<CsrfProtectionMiddleware>();
app.UseMiddleware<AuditLoggingMiddleware>();
app.UseAuthorization();
app.UseMiddleware<TenantResolutionMiddleware>();
app.UseMiddleware<PermissionEnforcementMiddleware>();

Loglarda PII Temizliği

PiiSanitizationMiddleware, kişisel olarak tanımlanabilir bilgilerin (PII) loglarımıza asla düşmemesini sağlamak için Serilog ile birlikte çalışır. Bu, kamu uygulamaları için bir uyumluluk gereksinimidir:

csharp
public class PiiSanitizationEnricher : ILogEventEnricher
{
    private static readonly Regex TcKimlikPattern =
        new(@"\b[1-9]\d{10}\b", RegexOptions.Compiled);

    private static readonly Regex EmailPattern =
        new(@"[\w.-]+@[\w.-]+\.\w+", RegexOptions.Compiled);

    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory factory)
    {
        var message = logEvent.RenderMessage();

        message = TcKimlikPattern.Replace(message, "[REDACTED-ID]");
        message = EmailPattern.Replace(message, "[REDACTED-EMAIL]");

        // Render edilmiş mesajı değiştir
        logEvent.AddOrUpdateProperty(
            factory.CreateProperty("SanitizedMessage", message));
    }
}

Cookie Tabanlı Kimlik Doğrulama İçin CSRF Koruması

Bir kullanıcı cookie'ler (session ya da httpOnly auth cookie'leri) üzerinden kimlik doğruladığında, CSRF koruması zorunludur. Middleware, durumu değiştiren her istekte anti-forgery token'ını doğrular:

csharp
public class CsrfProtectionMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // CSRF'yi yalnızca cookie ile kimlik doğrulanmış istekler için uygula
        if (!IsAuthenticatedViaCookie(context))
        {
            await next(context);
            return;
        }

        if (IsStateChangingMethod(context.Request.Method))
        {
            var csrfToken = context.Request.Headers["X-CSRF-TOKEN"]
                .FirstOrDefault();
            var cookieToken = context.Request.Cookies[".DigiFlow.CSRF"];

            if (string.IsNullOrEmpty(csrfToken) ||
                !CryptoHelper.ValidateTokenPair(csrfToken, cookieToken))
            {
                context.Response.StatusCode = 403;
                await context.Response.WriteAsJsonAsync(
                    new { error = "CSRF validation failed" });
                return;
            }
        }

        await next(context);
    }
}

Azure AD JWT Yapılandırması

JWT doğrulama yapılandırması katıdır — bir kamu uygulamasında yanlış yapılandırmaya yer yoktur:

csharp
services.AddAuthentication()
    .AddJwtBearer("Bearer", options =>
    {
        options.Authority = $"https://login.microsoftonline.com/{tenantId}/v2.0";
        options.Audience = configuration["AzureAd:ClientId"];

        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = $"https://login.microsoftonline.com/{tenantId}/v2.0",
            ValidateAudience = true,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromMinutes(2), // Varsayılan 5 dk'dan düşürüldü
            RequireSignedTokens = true,
            RequireExpirationTime = true,
        };

        options.Events = new JwtBearerEvents
        {
            OnTokenValidated = async context =>
            {
                // Claim'leri veritabanından gelen uygulamaya özel rollerle zenginleştir
                var userService = context.HttpContext.RequestServices
                    .GetRequiredService<IUserService>();
                var roles = await userService
                    .GetRolesAsync(context.Principal!.GetObjectId());

                var identity = context.Principal!.Identity as ClaimsIdentity;
                foreach (var role in roles)
                    identity!.AddClaim(new Claim(ClaimTypes.Role, role));
            },
        };
    });

OnTokenValidated olayı, Azure AD gruplarını uygulamaya özel izinlerimizle köprülediğimiz yerdir. Azure AD bize kullanıcının kim olduğunu söyler; veritabanımız ise ne yapabileceğini söyler.

Frontend Tarafı: CSRF'li httpOnly Cookie'ler

React frontend'inde, kimlik doğrulama token'ları localStorage'da saklamak yerine httpOnly cookie'leri kullanır. Bu, XSS tabanlı token hırsızlığının tüm bir sınıfını ortadan kaldırır:

typescript
// API istemcisi kurulumu
const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  withCredentials: true, // Her istekle cookie'leri gönder
});

// CSRF token interceptor'ı
apiClient.interceptors.request.use((config) => {
  if (['post', 'put', 'delete', 'patch'].includes(config.method ?? '')) {
    const csrfToken = getCsrfTokenFromCookie();
    if (csrfToken) {
      config.headers['X-CSRF-TOKEN'] = csrfToken;
    }
  }
  return config;
});

CSRF token'ı, JavaScript'in okuyabilmesi için sıradan (httpOnly olmayan) bir cookie'de saklanırken, asıl session token'ı JavaScript'in erişemeyeceği bir httpOnly cookie'dedir. Sunucu, her iki cookie'nin de mevcut olduğunu ve birbiriyle ilişkili olduğunu doğrular.

Birleşik Claims Principal

Bir kullanıcının nasıl kimlik doğruladığından bağımsız olarak, uygulama aynı claim kümesine sahip birleşik bir ClaimsPrincipal görür:

csharp
private AuthenticateResult EnrichWithClaims(
    AuthenticateResult original, string authMethod)
{
    var identity = original.Principal!.Identity as ClaimsIdentity;

    identity!.AddClaim(new Claim("auth_method", authMethod));
    identity.AddClaim(new Claim("auth_time",
        DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()));

    return AuthenticateResult.Success(
        new AuthenticationTicket(original.Principal!, original.Properties,
            Scheme.Name));
}

Controller'ların ve handler'ların, kullanıcının Azure AD, Windows Auth ya da session cookie üzerinden gelip gelmediğini bilmesine asla gerek yoktur. Sadece User.IsInRole("DocumentApprover") kontrolü yaparlar ve işe yarar.

Çıkarılan Dersler

  1. Tek bir kimlik doğrulama mekanizması bir lükstür. Kurumsal uygulamalar neredeyse her zaman birden fazlasına ihtiyaç duyar. Daha en baştan buna göre tasarlayın.
  2. httpOnly cookie'ler, token saklama konusunda localStorage'a üstündür — tarayıcı uygulamalarında. XSS saldırı yüzeyi dramatik şekilde küçülür.
  3. CSRF koruması yalnızca cookie kimlik doğrulaması için gereklidir. Bearer token istekleri doğası gereği CSRF'ye karşı güvenlidir, çünkü token açıkça eklenmek zorundadır.
  4. Log temizliği bir uyumluluk gereksinimidir, sahip-olunsa-iyi-olur bir şey değil. Bunu sonradan akla gelen bir ekleme olarak değil, loglama pipeline'ınıza dahil ederek inşa edin.
  5. JWT saat sapmasını (clock skew) azaltın. Varsayılan 5 dakikalık sapma fazla cömerttir. İki dakika, bir güvenlik penceresi oluşturmadan saat kaymasını ele almak için yeterlidir.