Mert Özen Her Satırda Daha İleri
Güvenlik Authentication ve Authorization — #18

ASP.NET Core 6'dan 11'e Authentication ve Identity'de Değişenler

Mert Özen 8 Kas 2026 10 dk 4 görüntülenme
ASP.NET Core 6'dan 11'e Authentication ve Identity'de Değişenler

ASP.NET Core 6'dan 10'a authentication'daki en önemli değişiklikler derleme hatası vermez, davranışı sessizce değiştirir. Aynı kodu .NET 6, 8 ve 10'da çalıştırıp tek scheme'in varsayılan olmasını, SecurityToken'ın JsonWebToken'a dönmesini, cookie'nin API'de 302 yerine 401 vermesini ve yeni metrikleri gösteriyor; .NET 9'un PAR'ını, .NET 11'in TimeProvider, passkey adı ve DBSC yeniliklerini ve bir geçiş kontrol listesini veriyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 6.0.36, 8.0.25 ve 10.0.13 runtime'larında aynı kaynakla derlenip denenmiştir; .NET 11 maddeleri Kasım 2026'daki RC (Release Candidate) dönemine ait release notes'tan alınmıştır, .NET 11 ile kod çalıştırılmamıştır.

Önce Tahmin Et

Bookstore'un 2022'de .NET 6 ile yazılmış bir iç servisi var: JWT (JSON Web Token) Bearer ile korunuyor, bir de cookie'li küçük bir yönetim paneli. Ekip onu .NET 10'a taşıyor; kod derleniyor, testler geçiyor. Aşağıdakilerden hangisi sessizce değişir? OnTokenValidated içinde context.SecurityToken'ı JwtSecurityToken'a cast eden kod. Giriş yapmamış kullanıcının panelden aldığı cevap. Bir de: AddAuthentication()'a scheme adı vermeyi unutmuş bir proje .NET 6'da çalışıyor muydu?

Bu yazı serinin son yazısı. 01 ile 17 arasında authentication ve authorization'ı parça parça kurduk; burada geriye bakıp ASP.NET Core 6'dan 11'e bu alanda nelerin değiştiğini sıralıyor ve en önemli davranış farklarını aynı kodu üç runtime'da çalıştırarak gösteriyoruz. ASP.NET Core Web API (Application Programming Interface) serisinin .NET 7, 8, 9 ve 10 yenilikleri yazıları her sürümün genel resmini çizdi; burada yalnız kimlik ve yetki tarafına bakıyoruz.

Deney Düzeneği: Aynı Kod, Üç Runtime

Tek bir konsol uygulaması iki küçük web uygulamasını aynı process'te ayağa kaldırıp kendine istek atıyor. Proje net8.0 ve net10.0'ı hedefliyor; .NET 6 için aynı dosyaları mcr.microsoft.com/dotnet/sdk:6.0 image'ından geçici bir container'da derledim. Kodu C# 10 ile derlenebilir tuttum, yani collection expression ya da primary constructor yok:

// 1) Yalnız JwtBearer kayıtlı; DEFAULT_SCHEME verilmezse AddAuthentication()'a scheme adı geçilmez
string? defaultScheme = Environment.GetEnvironmentVariable("DEFAULT_SCHEME");
WebApplicationBuilder jwtBuilder = WebApplication.CreateBuilder();
jwtBuilder.Logging.ClearProviders();
jwtBuilder.WebHost.UseUrls("http://127.0.0.1:19217");
AuthenticationBuilder auth = defaultScheme is null
    ? jwtBuilder.Services.AddAuthentication()
    : jwtBuilder.Services.AddAuthentication(defaultScheme);
auth.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidIssuer = "bookstore-lab",
        ValidAudience = "bookstore-api",
        IssuerSigningKey = key
    };
    options.Events = new JwtBearerEvents
    {
        OnTokenValidated = context =>
        {
            context.HttpContext.Items["tokenType"] = context.SecurityToken.GetType().Name;
            return Task.CompletedTask;
        }
    };
});

// 2) Cookie authentication, JSON dönen bir minimal API endpoint'i
cookieBuilder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
cookieApp.MapGet("/orders", () => new[] { new { Id = 1001, Total = 320m } }).RequireAuthorization();

Bir MeterListener da Microsoft.AspNetCore.Authentication ve Microsoft.AspNetCore.Authorization meter'larında yayımlanan instrument'ları topluyor. Signing key her çalıştırmada rastgele üretiliyor. Proje dosyasında paket sürümü hedef framework'e göre seçiliyor:

<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
  <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.36" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
  <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.33" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
  <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.14" />
</ItemGroup>

Üç runtime'ın çıktısı:

.NET 6.0.36
jwt, no token  : 500 InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).
jwt, with token: 500 InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).
cookie, no auth: 302 -> http://127.0.0.1:19218/Account/Login?ReturnUrl=%2Forders
auth metrics: (none)

.NET 6.0.36   (DEFAULT_SCHEME=Bearer)
jwt, no token  : 401
jwt, with token: 200 SecurityToken = JwtSecurityToken
cookie, no auth: 302 -> http://127.0.0.1:19218/Account/Login?ReturnUrl=%2Forders
auth metrics: (none)

.NET 8.0.25
jwt, no token  : 401
jwt, with token: 200 SecurityToken = JsonWebToken
cookie, no auth: 302 -> http://127.0.0.1:19218/Account/Login?ReturnUrl=%2Forders
auth metrics: (none)

.NET 10.0.13
jwt, no token  : 401
jwt, with token: 200 SecurityToken = JsonWebToken
cookie, no auth: 401 -> http://127.0.0.1:19218/Account/Login?ReturnUrl=%2Forders
auth metrics: aspnetcore.authentication.authenticate.duration, aspnetcore.authentication.challenges, aspnetcore.authentication.forbids, aspnetcore.authentication.sign_ins, aspnetcore.authentication.sign_outs, aspnetcore.authorization.attempts

Dört satır grubu, dört sürüm değişikliği. Aşağıda sırayla.

.NET 7: Tek Scheme Varsayılan Olur, user-jwts Gelir

.NET 6 çıktısındaki 500 hatası, Önce Tahmin Et'in son sorusunun cevabı: .NET 6'da yalnız bir scheme kayıtlıyken bile AddAuthentication()'a varsayılanı söylemezsen, korumalı her endpoint exception fırlatır. .NET 7 release notes'unda bunun başlığı "Authentication uses single scheme as DefaultScheme": tek bir scheme kayıtlıysa otomatik olarak varsayılan oluyor. .NET 8 ve 10 çıktısında aynı kod 401 dönüyor. Pratikteki etkisi: .NET 7+ projelerde bu satırı yazmadan çalışan kod, ikinci bir scheme eklendiği gün (örneğin JwtBearer'ın yanına cookie) bu otomatik davranışı kaybeder ve .NET 6'daki hatayı verir. Birden fazla scheme varsa varsayılanları her zaman açıkça yaz; 01. yazıdaki policy scheme tartışması bunun için.

.NET 7'nin diğer iki katkısı geliştirme ergonomisi. dotnet user-jwts aracı geliştirme için imzalı JWT üretiyor ve signing key'i user-secrets'ta saklıyor (bu seride kullanıcı profiline yazmamak için onun yerine key'i çalışma klasöründe ürettik). AddAuthorizationBuilder() ise policy tanımlarını zincirlenebilir hâle getirdi; 13. yazıdaki .AddPolicy("books.read", ...) zinciri onunla yazıldı ve API dokümanı bu method'un ASP.NET Core 7.0'dan beri var olduğunu gösteriyor.

.NET 8: Identity API Endpoint'leri, JsonWebTokenHandler ve IAuthorizationRequirementData

.NET 6 çıktısının ikinci grubunda (varsayılan scheme verilince) SecurityToken = JwtSecurityToken, .NET 8'de JsonWebToken. Bu, .NET 8 breaking changes listesindeki "Security token events return a JsonWebToken" maddesi: JwtBearer, OpenIdConnect ve WsFederation handler'ları varsayılan olarak JwtSecurityTokenHandler yerine daha hızlı JsonWebTokenHandler'ı kullanıyor. Property'nin tipi hâlâ SecurityToken, yani derleyici hiçbir şey söylemiyor; ama (JwtSecurityToken)context.SecurityToken yazan kod çalışma zamanında InvalidCastException fırlatıyor ya da as ile null alıp claim'leri sessizce kaçırıyor. Önce Tahmin Et'in ilk sorusunun cevabı bu. Çözüm cast'i JsonWebToken'a çevirmek; acil durumda JwtBearerOptions.UseSecurityTokenValidators = true eski davranışa döndürüyor ama bunu kalıcı çözüm sayma.

.NET 8'in Identity tarafındaki büyük yeniliği MapIdentityApi<TUser>(): SPA (Single-Page Application) ve mobil istemciler için kayıt, giriş, refresh, e-posta onayı ve 2FA (two-factor authentication) endpoint'leri. Ürettiği token JWT değil, Identity'ye özgü bir bearer token; sınırlarını 03. yazıda ve ASP.NET Core Web API serisinin .NET 8 yenilikleri yazısında konuştuk. Authorization tarafında IAuthorizationRequirementData geldi: parametreli bir policy için eskiden attribute, policy provider, requirement ve handler yazmak gerekirken artık requirement'ı doğrudan üreten tek bir attribute yetiyor. Bu mekanizmanın ayrıntısı ilerideki "ASP.NET Core Güvenliği" serisinin 01. yazısında.

.NET 9: OIDC'de PAR ve AdditionalAuthorizationParameters

.NET 9'un bu alandaki iki katkısını 11. yazıda kullandık. OpenIdConnect handler'ı PAR'ı (Pushed Authorization Requests, RFC (Request for Comments) 9126) destekliyor ve varsayılan davranış PushedAuthorizationBehavior.UseIfAvailable: authorization server discovery dokümanında PAR endpoint'i ilan ediyorsa handler onu kendiliğinden kullanıyor. Bu yükseltmede fark edilmeden gelen bir davranış değişikliği: .NET 8'den 9'a geçen ve Keycloak gibi PAR destekleyen bir sunucuya bağlanan uygulamanın authorization request'i, hiçbir kod değişmeden back channel'a taşınıyor. Genelde iyi bir şey; ama authorization server'ın önünde PAR endpoint'ini engelleyen bir WAF (Web Application Firewall) ya da gateway kuralı varsa giriş bozulur. AdditionalAuthorizationParameters ise eskiden OnRedirectToIdentityProvider event'inde elle eklenen prompt, audience gibi parametreleri tek satıra indirdi.

.NET 10: Passkey, API'ye 401/403 ve Metrikler

.NET 10 çıktısındaki cookie, no auth: 401 satırı, aynı kodun .NET 6 ve 8'de 302 döndüğü yerde. Release notes'taki başlığı "Avoid cookie login redirects for known API endpoints": cookie authentication, IApiEndpointMetadata taşıyan endpoint'lerde login sayfasına redirect yerine 401, access denied yerine 403 dönüyor. Bu metadata [ApiController]'lara, JSON okuyan ya da yazan minimal API endpoint'lerine, TypedResults dönenlere ve SignalR endpoint'lerine otomatik ekleniyor. Çıktıdaki ayrıntıya dikkat: Location header'ı hâlâ yazılıyor, yalnız durum kodu değişti. Önce Tahmin Et'in ikinci sorusunun cevabı bu: panelin JavaScript'i 302'yi takip edip login sayfasının HTML'ini (HyperText Markup Language) JSON diye parse etmeye çalışıyorduysa artık 401 görüyor. Bu çoğu zaman bir düzeltme, ama eski davranışa güvenen bir istemci varsa OnRedirectToLogin'i override ederek redirect'i geri getirebilirsin. 14. yazıdaki BFF'te (Backend for Frontend) ise tersini yapıp her durumda 401 döndürdük, çünkü YARP (Yet Another Reverse Proxy) route'ları bu metadata'yı taşımıyor.

Son satır .NET 10'un metrikleri: Microsoft.AspNetCore.Authentication meter'ı authenticate süresini, challenge, forbid, sign-in ve sign-out sayılarını; Microsoft.AspNetCore.Authorization meter'ı authorization denemelerini yayımlıyor. Identity de kendi Microsoft.AspNetCore.Identity meter'ında kullanıcı oluşturma, parola kontrolü, token üretme ve doğrulama, sign-in sayıları gibi metrikler veriyor. Bunlar OpenTelemetry ile dışarı aktarılabiliyor; ani bir challenge artışı bir saldırının ya da bozuk bir deploy'un ilk işareti olabilir.

Identity'nin .NET 10'daki büyük yeniliği passkey desteği: WebAuthn (Web Authentication) ve FIDO2 (Fast IDentity Online 2) tabanlı passwordless giriş, Blazor Web App şablonunda hazır geliyor. Angular gibi bir istemciyle kurulumunu 07. yazıda adım adım yaptık.

.NET 11: Ne Geliyor?

.NET 11 bu yazı yazılırken RC dönemindeydi: Microsoft'un destek sayfası RC3'ü (üçüncü Release Candidate) go-live sürüm olarak listeliyor ve onun desteği 10 Kasım 2026'da, GA (General Availability) sürümünün beklendiği gün bitiyor. Aşağıdakiler release notes'tan, kod çalıştırılmadan aktarılıyor. Identity artık zamanla ilgili bütün işlemlerde DateTime yerine TimeProvider kullanıyor; token ömrü, lockout süresi ve security stamp doğrulaması FakeTimeProvider ile test edilebilir hâle geliyor. Passkey'lerin görünen adı authenticator'ın AAGUID'inden (Authenticator Attestation Globally Unique Identifier) otomatik çıkarılıyor; Google Password Manager, iCloud Keychain, Windows Hello, 1Password ve Bitwarden için hazır eşlemeler var. dotnet user-jwts dosya tabanlı uygulamalarla --file seçeneğiyle çalışıyor.

Uzun ömürlü bağlantılarda authentication refresh geliyor: SignalR bağlantısı kopmadan token'ını yenileyebiliyor (hub başına EnableAuthenticationRefresh), Blazor Server circuit'leri yenilenen ClaimsPrincipal'ı yeniden bağlanmadan alıyor. IAuthorizationRequirementData ve AuthorizationPolicy metadata'sı MVC (Model-View-Controller), SignalR ve Blazor'un AuthorizeView'ında tutarlı uygulanıyor. Negotiate authentication HTTPS'te (HTTP Secure) TLS (Transport Layer Security) channel binding kullanıyor. Deneysel tarafta DBSC (Device Bound Session Credentials) için bir sunucu uygulaması var: Microsoft.AspNetCore.Authentication.DeviceBoundSessions paketi cookie authentication'ın üzerine oturuyor ve session'ın yenilenmesini tarayıcının elindeki bir private key'e bağlıyor; 13. yazıdaki DPoP'un (Demonstrating Proof of Possession) cookie dünyasındaki karşılığı gibi düşünebilirsin. Paket .NET 11 boyunca prerelease kalacak ve tarayıcı desteği henüz deneysel.

Geçiş Kontrol Listesi

Sürüm geçişiDeğişen davranışKontrol et
6 → 7Tek scheme otomatik varsayılanBirden çok scheme varsa varsayılanlar açıkça yazılı mı?
7 → 8SecurityToken artık JsonWebTokenJwtSecurityToken'a cast eden event kodu
7 → 8Claim mapping JsonWebTokenHandler üzerindenMapInboundClaims ayarı ve claim tipleri birebir aynı mı?
8 → 9PAR destekleyen IdP'de PAR otomatikGateway/WAF kuralları, PushedAuthorizationBehavior
9 → 10API endpoint'lerinde cookie 401/403302'ye güvenen istemciler, OnRedirectToLogin
9 → 10Authentication/authorization/Identity metrikleriDashboard ve alarmlar
10 → 11Identity TimeProvider kullanıyorZamanı DateTime.UtcNow ile taklit eden testler

Tabloyu bir test listesine çevirmenin en ucuz yolu bu yazıdaki gibi bir probe: authentication pipeline'ının dışarıdan görünen davranışını (durum kodları, header'lar, claim tipleri) yükseltmeden önce ve sonra aynı testlerle kaydet. Bu serinin her yazısındaki curl çıktıları da aslında böyle bir kayıt.

Üretimde Dikkat

Major sürüm yükseltmesini authentication açısından ayrı bir iş kalemi olarak planla: ilgili sürümlerin "breaking changes" sayfalarında Security başlığını tek tek oku. Microsoft.IdentityModel paketlerini (JsonWebTokens, Protocols.OpenIdConnect) ASP.NET Core'un getirdiği sürümle uyumlu tut; .NET 8 geçişinde claim'lerin kaybolduğunu bildiren GitHub kayıtlarının bir kısmında çözüm, projede elle sabitlenmiş eski bir IdentityModel paketini güncellemekti. Lab'daki OIDC (OpenID Connect) handler'ının istek header'larında x-client-ver=8.19.2.0 görünüyordu; senin projende ne yazıyor, bir kez bak.

LTS (Long-Term Support) ve STS (Standard Term Support) takvimini güvenlik açısından da düşün. Microsoft'un destek sayfasına göre .NET 8 (LTS) ve .NET 9 (STS) için destek 10 Kasım 2026'da, yani bu yazıdan iki gün sonra bitiyor; .NET 10 (LTS) 14 Kasım 2028'e kadar destekleniyor. .NET 6 ve 7 zaten güvenlik yaması almıyor. Authentication kodu en çok güvenlik yaması alan alanlardan biri; desteği biten bir sürümde kalmak, bu seride kurduğumuz her savunmanın altındaki zemini zayıflatır. Bu yazıdaki probe'un .NET 8 çıktısı bu yüzden bir yükseltme listesinin ilk satırı olmalı.

Sık Düşülen Tuzaklar

Birincisi, "derleniyor ve testler geçiyor" demeyi yükseltmenin bittiği anlamına almak; bu yazıdaki değişikliklerin hiçbiri derleme hatası üretmiyor. İkincisi, UseSecurityTokenValidators = true'yu kalıcı çözüm olarak bırakmak. Üçüncüsü, .NET 10'un 401 davranışını bir bug sanıp her yere redirect geri getirmek. Dördüncüsü, .NET 9'da PAR'ın otomatik açıldığını bilmeden giriş sorunlarını authorization server'da aramak. Beşincisi, preview ve deneysel paketleri (DBSC gibi) production'a taşımak.

Küçük Bir Deneme

★☆☆ Probe'a JwtBearerOptions.UseSecurityTokenValidators = true satırını ekle ve .NET 10'da SecurityToken'ın tipinin değiştiğini göster.

★★☆ Cookie uygulamasına Results.Text dönen ikinci bir endpoint ekle. .NET 10'da bu endpoint'in 302 mi 401 mi döndüğünü tahmin et, sonra çalıştırıp IApiEndpointMetadata'nın hangi endpoint'lere eklendiğini açıkla.

★★★ .NET 11 SDK'sı (Software Development Kit) GA olduğunda probe'a net11.0 hedefini ekle, Identity'yi FakeTimeProvider ile kurup bir parola sıfırlama token'ının ömrünü saati ileri sararak test et ve sonucu bu yazıdaki tabloya bir satır olarak ekle.

Seri burada bitiyor. Credential'dan session'a, cookie'den passkey'e, JWT'den refresh token'a, OAuth ve OIDC'den BFF'e ve yetki modellerine kadar her parçayı Bookstore üzerinde, gerçek çıktılarla kurduk. Kimlik doğrulamanın mekanizmasını burada bıraktık; ASP.NET Core'un authorization framework'ünün içini, policy provider ve handler zincirini ilerideki "ASP.NET Core Güvenliği" serisinde açacağız.