Authorization'ın İç Yapısı: Policy Provider, Requirement Handler'lar, IAuthorizationMiddlewareResultHandler ve Policy Testleri
AuthorizationMiddleware her istekte endpoint metadata'sını tek policy'de birleştirir, authenticate eder, handler'ları çalıştırır ve sonucu 401 ya da 403'e çevirir. Handler'lar arasındaki OR, requirement'lar arasındaki AND ve Fail()'in veto etkisini gerçek çıktılarla gösteriyor; dinamik policy provider, IAuthorizationRequirementData, ProblemDetails dönen 403 ve endpoint × kullanıcı test matrisi kuruyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10 (ASP.NET Core 10.0.13 runtime) ile derlenip denenmiştir; testler xUnit v3 4.0.2 ve Microsoft.AspNetCore.Mvc.Testing 10.0.14 ile çalıştırılmıştır.
Önce Tahmin Et
Bookstore'da iade endpoint'i için bir policy yazdık: kullanıcıda orders.refund permission'ı olmalı ve hesabı askıya alınmamış olmalı. Edsger'in token'ında orders.refund claim'i var, ama hesabı geçen hafta askıya alındı. Permission handler'ı Succeed() çağırıyor, hesap durumu handler'ı Fail() çağırıyor. İstek 200 mü döner, 403 mü? Peki üçüncü bir handler aynı requirement için yine Succeed() derse sonuç değişir mi? Cevabını tut; yazının ortasında gerçek çıktıya bakacağız.
Bu serinin konusu ASP.NET Core'un güvenlik araçlarının iç yapısı. OWASP'ın (Open Worldwide Application Security Project) Top 10 listesini anlatan Web Güvenliği serisinin "OWASP A01: Broken Access Control (1)" yazısında policy'leri, resource-based authorization'ı ve fallback policy ile "varsayılan olarak reddet" yaklaşımını görmüştük; ASP.NET Core Web API (Application Programming Interface) serisinin "Authorization: Roller, Policy'ler ve Resource-Based Yetkilendirme" yazısı da temel API'yi anlatıyor. Burada bir adım içeri giriyoruz: [Authorize] yazdığında framework'ün içinde hangi parçalar hangi sırayla çalışıyor, hangisini nerede değiştirebilirsin ve bunları nasıl test edersin. Hangi yetki modelini seçeceğin, yani RBAC (Role-Based Access Control), ABAC (Attribute-Based Access Control) ya da permission tabanlı bir model ise Authentication ve Authorization serisinin "Authorization Modelleri" yazısının konusu.
AuthorizationMiddleware Bir İstekte Ne Yapıyor?
Her şey AuthorizationMiddleware içinde olur. Routing endpoint'i seçtikten sonra middleware o endpoint'in metadata'sına bakar ve beş adımda bir karar üretir. Aşağıdaki akış doğrudan dotnet/aspnetcore deposundaki release/10.0 dalının kaynak kodundan çıkarıldı.

Birinci adımda endpoint'ten üç tür metadata toplanır: [Authorize] attribute'ları ve RequireAuthorization("...") çağrıları (IAuthorizeData), doğrudan eklenmiş AuthorizationPolicy object'leri ve .NET 8 ile gelen IAuthorizationRequirementData attribute'ları. İkinci adımda AuthorizationPolicy.CombineAsync bunları tek bir policy'de birleştirir. Policy adlarını çözmek için IAuthorizationPolicyProvider çağrılır. Birleşik policy, provider izin veriyorsa endpoint başına cache'lenir. Ortada hiç policy yoksa middleware hiçbir şey yapmadan next() çağırır; fallback policy tanımlamadıysan korumasız endpoint'ler buradan geçer.
Üçüncü adımda IPolicyEvaluator.AuthenticateAsync çalışır. Policy'de AuthenticationSchemes varsa yalnız o scheme'ler denenir, yoksa HttpContext.User olduğu gibi kullanılır. Endpoint [AllowAnonymous] taşıyorsa kontrol burada biter. Dikkat: authentication yine de çalışmıştır, yani anonim bir endpoint'te bile User doluysa onu okuyabilirsin.
Dördüncü adım asıl iştir: IAuthorizationService, policy'deki requirement'ları alır, IAuthorizationHandlerProvider'dan kayıtlı bütün handler'ları ister ve hepsini sırayla çalıştırır. Sonra IAuthorizationEvaluator sonucu değerlendirir. Beşinci adımda sonuç HTTP'ye (Hypertext Transfer Protocol) çevrilir: authentication başarısızsa Challenge (genelde 401), başarılı ama yetki yoksa Forbid (genelde 403). Bu son adımı yapan parça IAuthorizationMiddlewareResultHandler ve onu değiştirebiliyorsun.
Requirement ve Handler: OR, AND ve Fail()
Requirement yalnızca veri taşır, kararı handler verir. Bookstore'da iki requirement var:
using Microsoft.AspNetCore.Authorization;
namespace Bookstore.Api.Authorization;
public sealed record PermissionRequirement(string Permission) : IAuthorizationRequirement;
public sealed record ActiveAccountRequirement : IAuthorizationRequirement;
PermissionRequirement için iki ayrı handler yazıyoruz. Biri kullanıcıda permission claim'i arıyor; diğeri nöbetteki destek ekibinin iade yapabilmesine izin veriyor:
public sealed class PermissionClaimHandler : AuthorizationHandler<PermissionRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, PermissionRequirement requirement)
{
if (context.User.HasClaim("permission", requirement.Permission))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
public sealed class OnCallSupportHandler : AuthorizationHandler<PermissionRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, PermissionRequirement requirement)
{
if (requirement.Permission == "orders.refund"
&& context.User.IsInRole("support")
&& context.User.HasClaim("on_call", "true"))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Kuralları DefaultAuthorizationService ve DefaultAuthorizationEvaluator koyuyor. Kaynak kodda karar tek satır: HasSucceeded, yalnız Fail() hiç çağrılmadıysa, en az bir Succeed() çağrıldıysa ve bekleyen (pending) requirement kalmadıysa true olur. Buradan üç davranış çıkıyor:
- Aynı requirement için birden çok handler varsa aralarında OR vardır: biri
Succeed()derse requirement pending listesinden çıkar. - Bir policy'deki requirement'lar arasında AND vardır: her biri en az bir handler tarafından karşılanmalıdır.
Fail()veto'dur: başka handler'lar ne derse desin sonuç başarısızdır. "Succeed etmemek" ise sadece "ben bu konuda bir şey söylemiyorum" demektir.
Bu yüzden handler'da kural basit: emin değilsen hiçbir şey çağırma. Fail() yalnız "başka hiçbir kural bunu kurtaramaz" diyebildiğin durumlar içindir, askıya alınmış hesap gibi. AuthorizationOptions.InvokeHandlersAfterFailure varsayılan olarak true; yani Fail() sonrasında da kalan handler'lar çalışır. Pahalı handler'ların varsa bunu false yapıp gereksiz veritabanı çağrılarını kesebilirsin, ama o zaman log'da hangi requirement'ların eksik olduğunu daha az görürsün.
Handler'da DI ve Async Veri Erişimi
Hesap durumunu claim'e koymak cazip ama yanlış: token bir saat geçerliyse askıya alınan kullanıcı bir saat daha iade yapar. Durumu her istekte kaynaktan okuyan handler DI (dependency injection) ile bir store alıyor:
public sealed class ActiveAccountHandler(IAccountStatusStore statusStore)
: AuthorizationHandler<ActiveAccountRequirement>
{
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context, ActiveAccountRequirement requirement)
{
string? userName = context.User.FindFirstValue(ClaimTypes.Name);
if (userName is null)
{
return;
}
AccountStatus status = await statusStore.GetStatusAsync(userName);
if (status == AccountStatus.Suspended)
{
context.Fail(new AuthorizationFailureReason(this, "account_suspended"));
return;
}
context.Succeed(requirement);
}
}
Kayıt sırasında lifetime'a dikkat. Gerçek projede IAccountStatusStore bir DbContext kullanır ve scoped olur. Handler'ı alışkanlıkla singleton kaydedersen Development ortamında uygulama hiç ayağa kalkmaz:
Unhandled exception. System.AggregateException: Some services are not able to be constructed (...)
---> System.InvalidOperationException: Cannot consume scoped service 'Bookstore.Api.Authorization.IAccountStatusStore' from singleton 'Microsoft.AspNetCore.Authorization.IAuthorizationHandler'.
Development'ta scope doğrulaması açık olduğu için bu hata başlangıçta yakalanıyor. Production'da doğrulama kapalıdır; aynı kayıt orada sessizce captive dependency üretir, yani tek bir DbContext instance'ı bütün isteklerde paylaşılır. Doğru kayıt şöyle:
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler, PermissionClaimHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, OnCallSupportHandler>();
builder.Services.AddScoped<IAuthorizationHandler, ActiveAccountHandler>();
builder.Services.AddScoped<IAccountStatusStore, InMemoryAccountStatusStore>();
builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, ProblemDetailsAuthorizationResultHandler>();
DefaultAuthorizationHandlerProvider handler'ları IEnumerable<IAuthorizationHandler> olarak request scope'undan alır, bu yüzden scoped handler sorunsuz çalışır. Handler içinde CancellationToken yok; isteğin token'ına ihtiyacın varsa context.Resource üzerinden gelen HttpContext'in RequestAborted property'sini kullanabilirsin. Middleware, resource olarak varsayılan durumda HttpContext'i geçirir.
Dinamik Policy: IAuthorizationPolicyProvider
Permission sayısı büyüdükçe AddPolicy("orders.refund", ...), AddPolicy("orders.cancel", ...) satırları bitmez. IAuthorizationPolicyProvider, policy adını görünce policy'yi o anda üretmeni sağlar. Kuralımız: adı Permission: ile başlayan her policy, o permission'ı ve aktif hesabı ister.
public sealed class PermissionPolicyProvider(IOptions<AuthorizationOptions> options) : IAuthorizationPolicyProvider
{
public const string Prefix = "Permission:";
private readonly DefaultAuthorizationPolicyProvider _fallback = new(options);
public bool AllowsCachingPolicies => true;
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => _fallback.GetDefaultPolicyAsync();
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() => _fallback.GetFallbackPolicyAsync();
public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
if (!policyName.StartsWith(Prefix, StringComparison.Ordinal))
{
return _fallback.GetPolicyAsync(policyName);
}
AuthorizationPolicy policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddRequirements(new PermissionRequirement(policyName[Prefix.Length..]), new ActiveAccountRequirement())
.Build();
return Task.FromResult<AuthorizationPolicy?>(policy);
}
}
Endpoint tarafında yalnız ad yazılıyor: .RequireAuthorization("Permission:orders.cancel"). Provider'ın üç detayı önemli. Birincisi, tanımadığın adları DefaultAuthorizationPolicyProvider'a devretmezsen AddPolicy ile tanımladığın "Refund" gibi statik policy'ler kaybolur. İkincisi, DI container'da tek bir IAuthorizationPolicyProvider olur; seninki varsayılanın yerine geçer. Üçüncüsü, AllowsCachingPolicies. Interface'te bu property'nin varsayılanı false; DefaultAuthorizationPolicyProvider ise yalnız kendi tipi için true döner. Yani bu satırı yazmazsan middleware policy'yi cache'lemez ve her istekte provider'ı yeniden çağırır. Provider'a geçici bir Console.WriteLine koyup test matrisindeki iki cancel isteğini çalıştırdım: AllowsCachingPolicies => false iken provider 2 kez, true iken 1 kez çağrıldı. Policy'n isteğe göre değişmiyorsa (tenant'a göre değişen bir policy değilse) true dön.
Provider'sız Yol: IAuthorizationRequirementData
.NET 8, çoğu durumda provider yazma ihtiyacını ortadan kaldıran IAuthorizationRequirementData interface'ini getirdi. Attribute, requirement'larını doğrudan kendisi üretir; middleware bunları birinci adımda toplayıp policy'ye ekler:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public sealed class RequirePermissionAttribute(string permission)
: AuthorizeAttribute, IAuthorizationRequirementData
{
public string Permission { get; } = permission;
public IEnumerable<IAuthorizationRequirement> GetRequirements() =>
[new PermissionRequirement(Permission), new ActiveAccountRequirement()];
}
app.MapPost("/orders/{id:int}/refund", (int id) => Results.Ok(new { id, status = "refunded" }))
.RequireAuthorization("Refund");
app.MapPost("/orders/{id:int}/cancel", (int id) => Results.Ok(new { id, status = "cancelled" }))
.RequireAuthorization("Permission:orders.cancel");
app.MapDelete("/books/{id:int}", [RequirePermission("books.delete")] (int id) => Results.NoContent());
Attribute AuthorizeAttribute'tan türediği için IAuthorizeData olarak da sayılır ve varsayılan policy'yi (authenticated user) getirir; requirement'lar onun üstüne eklenir. Provider'a göre farkı şu: string parse etmiyorsun, permission adı derleme anında tip güvenli bir argüman. .NET 10'da bu attribute'ları middleware okuyor; yani minimal API ve endpoint routing ile çalışan controller action'larında geçerli. MVC (Model-View-Controller) filter'larında, SignalR hub'larında ve Blazor'ın AuthorizeView'unda tutarlı desteği .NET 11 getiriyor; serinin son yazısında bu değişikliğe döneceğiz.
403'e Gövde Vermek: IAuthorizationMiddlewareResultHandler
Varsayılan davranışta Forbid, authentication handler'ının ForbidAsync method'unu çağırır: cookie handler'ı tarayıcı isteklerinde AccessDenied sayfasına redirect eder, JWT (JSON Web Token) bearer handler'ı boş gövdeli bir 403 döner. API istemcisi neden reddedildiğini bilemez. IAuthorizationMiddlewareResultHandler beşinci adımı devralmanı sağlar:
public sealed class ProblemDetailsAuthorizationResultHandler(
IProblemDetailsService problemDetailsService,
ILogger<ProblemDetailsAuthorizationResultHandler> logger) : IAuthorizationMiddlewareResultHandler
{
private readonly AuthorizationMiddlewareResultHandler _defaultHandler = new();
public async Task HandleAsync(
RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
{
if (!authorizeResult.Forbidden)
{
await _defaultHandler.HandleAsync(next, context, policy, authorizeResult);
return;
}
AuthorizationFailure? failure = authorizeResult.AuthorizationFailure;
string[] reasons = failure?.FailureReasons.Select(r => r.Message).ToArray() ?? [];
string[] pending = failure?.FailedRequirements.Select(r => r.GetType().Name).ToArray() ?? [];
logger.LogWarning("Forbidden {Path} for {User}. Pending: {Pending}. Reasons: {Reasons}",
context.Request.Path, context.User.Identity?.Name, pending, reasons);
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await problemDetailsService.WriteAsync(new ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Status = StatusCodes.Status403Forbidden,
Title = "Forbidden",
Detail = "You are not allowed to perform this operation.",
Extensions = { ["reasons"] = reasons }
}
});
}
}
Success ve Challenge durumlarını varsayılan AuthorizationMiddlewareResultHandler'a bırakıyoruz; yalnız Forbid'i değiştiriyoruz. Şimdi tahmin sorusunun cevabı. Edsger'in iade isteğinin gerçek log'u ve yanıtı:
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. Fail() was explicitly called.
warn: Bookstore.Api.Authorization.ProblemDetailsAuthorizationResultHandler[0]
Forbidden /orders/1/refund for edsger. Pending: . Reasons: account_suspended
403 application/problem+json
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.4","title":"Forbidden","status":403,"detail":"You are not allowed to perform this operation.","reasons":["account_suspended"],"traceId":"00-d083ea5cc1892f022e6786097695ede6-c48742339a24c269-00"}
Fail() kazandı: permission handler'ının Succeed() demesi bir şey değiştirmedi. Bir detay daha var: "Pending" listesi boş. Fail() çağrıldığında AuthorizationFailure yalnız FailureReasons taşır, FailedRequirements boş gelir. Permission'ı olmayan Alan için durum tersi; log eksik requirement'ı yazar, reason listesi boştur:
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
PermissionRequirement { Permission = orders.refund }
warn: Bookstore.Api.Authorization.ProblemDetailsAuthorizationResultHandler[0]
Forbidden /orders/1/refund for alan. Pending: PermissionRequirement. Reasons:
Requirement'ları record yapmanın küçük bir faydası burada görünüyor: log satırındaki PermissionRequirement { Permission = orders.refund } metni record'un ürettiği ToString() çıktısı.
Policy'leri Test Etmek
Authorization kodu iki seviyede test edilir. Handler'lar saf mantıktır, AuthorizationHandlerContext'i elle kurup unit test yazarsın:
[Fact]
public async Task Fail_wins_over_succeed()
{
PermissionRequirement permission = new("orders.refund");
ActiveAccountRequirement active = new();
AuthorizationHandlerContext context = new(
[permission, active],
User(new Claim(ClaimTypes.Name, "edsger"), new Claim("permission", "orders.refund")),
null);
await new PermissionClaimHandler().HandleAsync(context);
await new ActiveAccountHandler(new InMemoryAccountStatusStore()).HandleAsync(context);
Assert.Empty(context.PendingRequirements.OfType<PermissionRequirement>());
Assert.True(context.HasFailed);
Assert.False(context.HasSucceeded);
Assert.Equal("account_suspended", Assert.Single(context.FailureReasons).Message);
}
Ama handler'ların doğru endpoint'e bağlandığını, provider'ın adı doğru çözdüğünü ve result handler'ın gerçekten 403 yazdığını yalnız uçtan uca test gösterir. ASP.NET Core Web API serisinin "Integration Test" yazısındaki WebApplicationFactory'yi kullanıyoruz, gerçek authentication yerine header'dan claim okuyan bir test scheme'i koyuyoruz:
public sealed class TestAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder)
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
{
public const string SchemeName = "Test";
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
// X-Test-User: alan ; X-Test-Claims: role=support;on_call=true
if (!Request.Headers.TryGetValue("X-Test-User", out var userName))
{
return Task.FromResult(AuthenticateResult.NoResult());
}
List<Claim> claims = [new(ClaimTypes.Name, userName.ToString())];
foreach (string pair in Request.Headers["X-Test-Claims"].ToString()
.Split(';', StringSplitOptions.RemoveEmptyEntries))
{
string[] parts = pair.Split('=', 2);
string type = parts[0] == "role" ? ClaimTypes.Role : parts[0];
claims.Add(new Claim(type, parts[1]));
}
ClaimsPrincipal principal = new(new ClaimsIdentity(claims, SchemeName));
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, SchemeName)));
}
}
public sealed class BookstoreFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
services.AddAuthentication(TestAuthHandler.SchemeName)
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(TestAuthHandler.SchemeName, _ => { }));
}
}
Bu scheme yalnız test projesinde yaşar; uygulama koduna bir "test header'ı" sızmaz. Sonra endpoint × kullanıcı matrisini tek bir Theory ile yazıyoruz:
public static TheoryData<string?, string, string, string, HttpStatusCode> Matrix => new()
{
{ null, "", "POST", "/orders/1/refund", HttpStatusCode.Unauthorized },
{ "alan", "role=customer", "POST", "/orders/1/refund", HttpStatusCode.Forbidden },
{ "ada", "permission=orders.refund", "POST", "/orders/1/refund", HttpStatusCode.OK },
{ "grace", "role=support;on_call=true", "POST", "/orders/1/refund", HttpStatusCode.OK },
{ "grace", "role=support", "POST", "/orders/1/refund", HttpStatusCode.Forbidden },
{ "edsger", "permission=orders.refund", "POST", "/orders/1/refund", HttpStatusCode.Forbidden },
{ "ada", "permission=orders.cancel", "POST", "/orders/1/cancel", HttpStatusCode.OK },
{ "grace", "role=support;on_call=true", "POST", "/orders/1/cancel", HttpStatusCode.Forbidden },
{ "ada", "permission=books.delete", "DELETE", "/books/1", HttpStatusCode.NoContent },
{ "alan", "role=customer", "DELETE", "/books/1", HttpStatusCode.Forbidden },
{ null, "", "GET", "/books", HttpStatusCode.OK },
};
$ dotnet test --project Bookstore.Api.Tests
Test run summary: Passed!
total: 16
failed: 0
succeeded: 16
skipped: 0
duration: 1s 125ms
Matris, yetki kurallarının belgesi gibi okunuyor. Nöbetteki Grace iade yapabiliyor ama sipariş iptal edemiyor; çünkü OnCallSupportHandler yalnız orders.refund için konuşuyor. Yeni bir endpoint eklendiğinde matrise satır eklemek, code review'da en kolay sorulacak sorudur.
Üretimde Dikkat
Handler'lar her yetkili istekte çalışır. ActiveAccountHandler gibi veri okuyan bir handler, iade endpoint'ine gelen her istek için bir sorgu demek. Bunu handler'daki sorgunun süresinden ölç (EF Core yani Entity Framework Core ya da Dapper sorgularını trace'lerde zaten görüyorsan orada); gerekirse hesap durumunu birkaç saniyelik bir memory cache'te tut. Burada bilinçli bir takas var: cache süresi, askıya alınan bir hesabın en fazla ne kadar daha işlem yapabileceğini belirler.
Policy provider'ında GetPolicyAsync içinde veritabanına gidiyorsan AllowsCachingPolicies => false ile her istekte o sorguyu çalıştırırsın. Policy'yi dinamik yapan şey veritabanındaki permission listesi değil, policy adı olmalı; kullanıcıya özgü veriyi handler'da oku.
Reddedilen istekleri log'luyoruz, ama reasons alanında yalnız istemciye gösterilmesinde sakınca olmayan kodlar olmalı. account_suspended makul; "IP (Internet Protocol) adresi kara listede" gibi bir neden saldırgana bilgi verir. Ayrıntıyı log'a yaz, istemciye kod ver. 401 ve 403 sayılarının metriklerle nasıl izleneceğini serinin "Güvenlik Olaylarını Gözlemlemek" yazısında göreceğiz.
Sık Düşülen Tuzaklar
Policy adında yazım hatası derleme anında yakalanmaz. "Refnud" adını isteyen kod ilk istekte InvalidOperationException fırlatır; testteki gerçek mesaj No policy found: Refnud. oldu. Ad sabitlerini bir static class içinde topla ya da IAuthorizationRequirementData attribute'u kullan; matristeki her satır da bu hatayı yakalar.
Requirement'ı reddetmek için Fail() yerine "Succeed çağırmamak" yeterliyken her yerde Fail() kullanmak OR davranışını bozar. Grace'in nöbetçi olmadığı için OnCallSupportHandler Fail() çağırsaydı, permission claim'i olan bir destek çalışanı da iade yapamazdı.
Handler'ın kontrol ettiği requirement tipini yanlış yazmak sessiz bir hatadır: AuthorizationHandler<PermissionRequirement> yerine başka bir tip yazarsan handler hiç çağrılmaz, requirement pending kalır ve herkes 403 alır. Matris testi bunu ilk koşuda gösterir.
Custom provider yazıp _fallback'e devretmeyi unutmak, AddPolicy ile tanımlanmış policy'leri ve fallback policy'yi kaybettirir. Fallback policy kaybolursa [Authorize] taşımayan endpoint'ler sessizce herkese açılır.
Küçük Bir Deneme
★☆☆ AuthorizationOptions.InvokeHandlersAfterFailure değerini false yap, ActiveAccountHandler'ı PermissionClaimHandler'dan önce kaydet ve Edsger'in isteğini tekrar gönder. Log'daki mesaj ve FailureReasons değişiyor mu?
★★☆ RequirePermissionAttribute'u bir route group'a (MapGroup("/admin").WithMetadata(...)) uygula ve grubun içindeki bir endpoint'e ikinci bir [RequirePermission] ekle. İki permission arasında AND mi, OR mu oluştuğunu matrise satır ekleyerek doğrula.
★★★ PermissionPolicyProvider'ı tenant'a göre değişen policy'ler üretecek şekilde genişlet (Permission:orders.refund@tenant) ve AllowsCachingPolicies değerinin bu durumda neden önemli olduğunu ölç.
Authorization kararını verdik; bir sonraki yazıda bu kararın dayandığı cookie'leri ve antiforgery token'larını şifreleyen, ama çoğu zaman yalnız bir sorun çıktığında fark edilen alt sisteme, Data Protection'ın key ring'ine geçiyoruz.