August 5, 2026
ASP.NET Core’da Yetkilendirme: Role, Claim, Policy ve Resource-Based Authorization
Bir kullanıcı sisteme başarıyla giriş yaptı.

By Burak Emre İLERİ
11 min read
JWT doğrulandı. Kullanıcının kimliği belirlendi. React uygulaması açıldı ve kullanıcı dashboard ekranını görebiliyor.
Peki bu kullanıcı:
- Bütün müşteri kayıtlarını görebilir mi?
- Yalnızca kendisine atanmış kayıtları mı düzenleyebilir?
- Başka bir çalışanın oluşturduğu kaydı silebilir mi?
- URL'deki kayıt ID'sini değiştirerek farklı bir müşteriye erişebilir mi?
- React tarafında gizlenen bir butonu görünür hâle getirirse işlemi gerçekleştirebilir mi?
Adminrolünü tarayıcı belleğinde değiştirirse gerçekten admin olabilir mi?
Bu soruların cevabını authentication değil, authorization verir.
ASP.NET Core'da authentication kullanıcının kim olduğunu belirlerken authorization, bu kullanıcının belirli bir kaynağa erişip erişemeyeceğini belirler. Framework authentication işlemlerini handler ve scheme yapılarıyla, authorization işlemlerini ise policy, requirement ve handler bileşenleriyle yönetir.
1. Authentication ile Authorization Aynı Şey Değildir
Bu iki kavram sık sık birbirine karıştırılır.
Authentication
Authentication şu soruya cevap verir:
Bu kullanıcı kim?
Örneğin kullanıcı giriş yaptıktan sonra sistem şunları belirleyebilir:
Kullanıcı ID: 42
Ad: Burak Emre
Tenant ID: 7
Rol: SalesRepresentativeKullanıcı ID: 42
Ad: Burak Emre
Tenant ID: 7
Rol: SalesRepresentativeJWT, cookie, Microsoft Entra ID veya başka bir authentication yöntemi kullanıcının kimliğinin belirlenmesini sağlar.
Authorization
Authorization ise şu soruya cevap verir:
Bu kullanıcı bu işlemi gerçekleştirebilir mi?
Örneğin:
crm.read
crm.create
crm.update
crm.delete
crm.assign
crm.read.allcrm.read
crm.create
crm.update
crm.delete
crm.assign
crm.read.allBir kullanıcının sisteme giriş yapabilmesi, bütün endpoint'lere erişebileceği anlamına gelmez.
Authentication kapının giriş kartıdır. Authorization ise binanın hangi odalarına girebileceğinizi belirler.
2. Yalnızca [Authorize] Kullanmak Neden Yeterli Değildir?
Şu endpoint'i düşünelim:
[Authorize]
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(
int id,
CancellationToken cancellationToken)
{
var customer = await _db.Customers
.FindAsync([id], cancellationToken);
if (customer is null)
return NotFound();
_db.Customers.Remove(customer);
await _db.SaveChangesAsync(cancellationToken);
return NoContent();
}[Authorize]
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(
int id,
CancellationToken cancellationToken)
{
var customer = await _db.Customers
.FindAsync([id], cancellationToken);
if (customer is null)
return NotFound();
_db.Customers.Remove(customer);
await _db.SaveChangesAsync(cancellationToken);
return NoContent();
}Buradaki [Authorize] yalnızca kullanıcının giriş yapmış olmasını zorunlu tutar.
Aşağıdaki kullanıcıların tamamı endpoint'e erişebilir:
- Admin
- Satış temsilcisi
- Operasyon çalışanı
- Stajyer
- Yalnızca görüntüleme yetkisi olan kullanıcı
Kullanıcı geçerli bir authentication bilgisine sahipse silme işlemi çalışabilir.
[Authorize], tek başına "bu kullanıcı bu işlemi yapabilir" anlamına gelmez. Yalnızca "bu kullanıcı doğrulanmış" anlamına gelir.
Endpoint'in gerçek bir yetki kontrolüne ihtiyacı vardır:
[Authorize(Policy = Permissions.CustomerDelete)]
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(
int id,
CancellationToken cancellationToken)
{
// ...
}[Authorize(Policy = Permissions.CustomerDelete)]
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(
int id,
CancellationToken cancellationToken)
{
// ...
}3. Role, Claim, Permission ve Policy Arasındaki Fark
Bu kavramlar benzer görünse de farklı sorumluluklara sahiptir.
Role
Role, kullanıcının organizasyon içindeki genel grubunu ifade eder.
Örnekler:
Admin
SalesRepresentative
SalesManager
Operations
AccountingAdmin
SalesRepresentative
SalesManager
Operations
AccountingASP.NET Core role-based authorization kontrollerini [Authorize(Roles = "...")] biçiminde deklaratif olarak uygulayabilir.
[Authorize(Roles = "Admin")]
public IActionResult AdminPanel()
{
return View();
}[Authorize(Roles = "Admin")]
public IActionResult AdminPanel()
{
return View();
}Birden fazla rol virgülle yazıldığında roller arasında veya ilişkisi bulunur:
[Authorize(Roles = "Admin,SalesManager")][Authorize(Roles = "Admin,SalesManager")]Bu endpoint'e Admin veya SalesManager rolündeki kullanıcı erişebilir.
Birden fazla [Authorize] attribute'u ayrı ayrı kullanılırsa şartların birlikte karşılanması gerekir:
[Authorize(Roles = "Admin")]
[Authorize(Roles = "SalesManager")][Authorize(Roles = "Admin")]
[Authorize(Roles = "SalesManager")]Bu durumda kullanıcının iki rolde de bulunması gerekir.
Role kullanımındaki temel problem
Rol, kullanıcının kim olduğunu veya organizasyondaki yerini anlatır. Yapabileceği her işlemi ayrıntılı biçimde anlatmaz.
Örneğin SalesRepresentative rolündeki bir kullanıcı:
- Müşteri kaydı oluşturabilir.
- Kendi müşterisini güncelleyebilir.
- Başkasının müşterisini göremeyebilir.
- Atama değiştiremeyebilir.
- Müşteri silemeyebilir.
Bütün bunları yalnızca SalesRepresentative isminden anlayamayız.
Claim
Claim, kullanıcı veya başka bir subject hakkındaki bir isim-değer bilgisidir.
sub = 42
tenant_id = 7
department = Sales
country = TR
permission = crm.updatesub = 42
tenant_id = 7
department = Sales
country = TR
permission = crm.updateMicrosoft'un tanımına göre claim, subject'in ne yapabileceğinden ziyade subject hakkında bir bilgiyi temsil eden isim-değer çiftidir. Uygulama bu claim'leri authorization kararları verirken değerlendirebilir.
ASP.NET Core'da claim kontrolü şöyle yapılabilir:
User.HasClaim("department", "Sales");User.HasClaim("department", "Sales");Policy içinde de claim zorunlu tutulabilir:
options.AddPolicy(
"SalesDepartment",
policy =>
{
policy.RequireClaim("department", "Sales");
});options.AddPolicy(
"SalesDepartment",
policy =>
{
policy.RequireClaim("department", "Sales");
});Permission
Permission, kullanıcının uygulamada gerçekleştirebileceği belirli bir işlemi ifade eder.
crm.read
crm.create
crm.update
crm.delete
crm.assign
crm.transfer
task.manage
user.role.updatecrm.read
crm.create
crm.update
crm.delete
crm.assign
crm.transfer
task.manage
user.role.updatePermission, JWT standardının veya ASP.NET Core'un zorunlu tuttuğu özel bir kavram değildir. Uygulamanın oluşturduğu bir authorization modelidir.
Permission'lar claim olarak taşınabilir:
{
"sub": "42",
"permission": [
"crm.read",
"crm.create",
"crm.update"
]
}{
"sub": "42",
"permission": [
"crm.read",
"crm.create",
"crm.update"
]
}Ya da token'a bütün permission'ları yazmak yerine kullanıcı veya rol bilgileri üzerinden sunucu tarafında hesaplanabilir.
Role geniş bir organizasyon tanımıdır. Permission ise belirli bir işleme verilen yetkidir.
Policy
Policy, erişim için karşılanması gereken kuralların bir araya getirilmesidir.
Örneğin bir CRM kaydını güncellemek için şu şartları isteyebiliriz:
- Kullanıcı giriş yapmış olmalı.
crm.updatepermission'ına sahip olmalı.- Kullanıcı ile kayıt aynı tenant'a ait olmalı.
- Kullanıcı kayıt üzerinde yetkili olmalı.
- Kayıt kilitli olmamalı.
ASP.NET Core policy-based authorization modelinde policy'ler requirement'lardan oluşur ve handler'lar bu requirement'ları değerlendirir. Bir policy'de birden fazla requirement varsa policy'nin başarılı olması için bütün requirement'ların geçmesi gerekir.
4. Neden Endpoint'leri Role Yerine Permission ile Korumalıyız?
Şu kullanım başlangıçta kolay görünür:
[Authorize(Roles = "Admin,SalesManager")]
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(...)
{
// ...
}[Authorize(Roles = "Admin,SalesManager")]
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(...)
{
// ...
}Ancak bir süre sonra yeni bir rol gelir:
InsideSalesManagerInsideSalesManagerDaha sonra başka bir rol gelir:
RegionalManagerRegionalManagerArdından belirli şartlarda satış temsilcisinin de güncelleme yapması istenir.
Attribute büyümeye başlar:
[Authorize(
Roles =
"Admin,SalesManager,InsideSalesManager,RegionalManager")][Authorize(
Roles =
"Admin,SalesManager,InsideSalesManager,RegionalManager")]Sistemde yüzlerce endpoint varsa role isimleri bütün controller'lara dağılır.
Rol isimlerinden biri değiştirildiğinde birçok endpoint etkilenebilir.
Daha sürdürülebilir yaklaşım:
[Authorize(Policy = Permissions.CrmUpdate)][Authorize(Policy = Permissions.CrmUpdate)]Hangi rollerin crm.update permission'ına sahip olduğu merkezi olarak yönetilir.
Admin → crm.update
SalesManager → crm.update
InsideSalesManager → crm.update
SalesRepresentative → crm.update.ownAdmin → crm.update
SalesManager → crm.update
InsideSalesManager → crm.update
SalesRepresentative → crm.update.ownBöylece endpoint organizasyon yapısını bilmez.
Endpoint yalnızca ihtiyaç duyduğu yetkiyi bilir:
Bu işlem için
crm.updategerekiyor.
5. Permission Sabitlerinin Oluşturulması
Permission isimleri kodun farklı yerlerinde string olarak tekrar edilmemelidir.
Yanlış:
[Authorize(Policy = "crm.updtae")][Authorize(Policy = "crm.updtae")]Burada yazım hatası derleme zamanında anlaşılmaz.
Daha güvenli yaklaşım:
public static class Permissions
{
public const string CrmRead = "crm.read";
public const string CrmReadAll = "crm.read.all";
public const string CrmCreate = "crm.create";
public const string CrmUpdate = "crm.update";
public const string CrmDelete = "crm.delete";
public const string CrmAssign = "crm.assign";
public static readonly IReadOnlyCollection<string> All =
[
CrmRead,
CrmReadAll,
CrmCreate,
CrmUpdate,
CrmDelete,
CrmAssign
];
}public static class Permissions
{
public const string CrmRead = "crm.read";
public const string CrmReadAll = "crm.read.all";
public const string CrmCreate = "crm.create";
public const string CrmUpdate = "crm.update";
public const string CrmDelete = "crm.delete";
public const string CrmAssign = "crm.assign";
public static readonly IReadOnlyCollection<string> All =
[
CrmRead,
CrmReadAll,
CrmCreate,
CrmUpdate,
CrmDelete,
CrmAssign
];
}Kullanım:
[Authorize(Policy = Permissions.CrmUpdate)][Authorize(Policy = Permissions.CrmUpdate)]Permission adlarının tek bir kaynaktan yönetilmesi hem yazım hatalarını hem de sistem genelindeki isim tutarsızlıklarını azaltır.
6. Custom Permission Requirement Oluşturmak
Öncelikle bir authorization requirement tanımlayalım:
using Microsoft.AspNetCore.Authorization;
public sealed record PermissionRequirement(
string Permission) : IAuthorizationRequirement;using Microsoft.AspNetCore.Authorization;
public sealed record PermissionRequirement(
string Permission) : IAuthorizationRequirement;Requirement yalnızca değerlendirilmesi gereken bilgiyi taşır.
Şimdi requirement'ı değerlendirecek handler'ı oluşturalım:
using Microsoft.AspNetCore.Authorization;
public sealed class PermissionAuthorizationHandler
: AuthorizationHandler<PermissionRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
bool hasPermission = context.User.HasClaim(
claim =>
claim.Type == "permission" &&
claim.Value == requirement.Permission);
if (hasPermission)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}using Microsoft.AspNetCore.Authorization;
public sealed class PermissionAuthorizationHandler
: AuthorizationHandler<PermissionRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
bool hasPermission = context.User.HasClaim(
claim =>
claim.Type == "permission" &&
claim.Value == requirement.Permission);
if (hasPermission)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}Handler'ın görevi şudur:
- Kullanıcının claim'lerini inceler.
- İstenen permission mevcutsa requirement'ı başarılı kabul eder.
- Permission yoksa başarılı sonucu işaretlemez.
- Authorization sistemi isteği reddeder.
Handler'ı dependency injection sistemine kaydederiz:
builder.Services.AddSingleton<
IAuthorizationHandler,
PermissionAuthorizationHandler>();builder.Services.AddSingleton<
IAuthorizationHandler,
PermissionAuthorizationHandler>();Bu handler dış bir scoped servis kullanmadığı için singleton olabilir. Handler içerisinde DbContext gibi scoped bir dependency kullanılacaksa yaşam süresi buna uygun seçilmelidir.
7. Permission Policy'lerini Kaydetmek
Permission'ları policy olarak kaydedelim:
builder.Services.AddAuthorization(options =>
{
foreach (string permission in Permissions.All)
{
options.AddPolicy(
permission,
policy =>
{
policy.RequireAuthenticatedUser();
policy.AddRequirements(
new PermissionRequirement(permission));
});
}
});builder.Services.AddAuthorization(options =>
{
foreach (string permission in Permissions.All)
{
options.AddPolicy(
permission,
policy =>
{
policy.RequireAuthenticatedUser();
policy.AddRequirements(
new PermissionRequirement(permission));
});
}
});Bundan sonra endpoint üzerinde permission kullanabiliriz:
[Authorize(Policy = Permissions.CrmCreate)]
[HttpPost]
public async Task<IActionResult> Create(
CreateCrmRequest request,
CancellationToken cancellationToken)
{
// ...
}
[Authorize(Policy = Permissions.CrmDelete)]
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(
int id,
CancellationToken cancellationToken)
{
// ...
}[Authorize(Policy = Permissions.CrmCreate)]
[HttpPost]
public async Task<IActionResult> Create(
CreateCrmRequest request,
CancellationToken cancellationToken)
{
// ...
}
[Authorize(Policy = Permissions.CrmDelete)]
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(
int id,
CancellationToken cancellationToken)
{
// ...
}Bu kontrol route seviyesinde şu soruyu cevaplar:
Kullanıcının genel olarak bu işlemi gerçekleştirme permission'ı var mı?
Ancak henüz şu soruya cevap vermedik:
Kullanıcı bu belirli kayıt üzerinde işlem yapabilir mi?
Bunun için resource-based authorization gerekir.
8. Resource-Based Authorization Nedir?
Şu kurala sahip olduğumuzu düşünelim:
Satış temsilcisi yalnızca kendisine atanmış CRM kayıtlarını güncelleyebilir. Yönetici ise aynı tenant içindeki bütün kayıtları güncelleyebilir.
Burada yalnızca kullanıcı bilgisi yeterli değildir.
Karar verebilmek için hem kullanıcıyı hem CRM kaydını bilmemiz gerekir:
Kullanıcı ID = 42
CRM AssignedUserId = 42
CRM TenantId = 7
Kullanıcı TenantId = 7Kullanıcı ID = 42
CRM AssignedUserId = 42
CRM TenantId = 7
Kullanıcı TenantId = 7[Authorize] attribute'u action çalışmadan ve kaynak veritabanından yüklenmeden önce değerlendirilir. Bu nedenle belirli bir kaydın sahibi, tenant'ı veya durumu üzerinden karar verilmesi gereken senaryolarda yalnızca attribute kullanmak yeterli değildir; authorization işlemi kaynak yüklendikten sonra IAuthorizationService üzerinden yapılmalıdır.
9. CRM Kaydı İçin Resource Requirement
using Microsoft.AspNetCore.Authorization;
public sealed record EditCrmRequirement
: IAuthorizationRequirement;using Microsoft.AspNetCore.Authorization;
public sealed record EditCrmRequirement
: IAuthorizationRequirement;Handler:
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
public sealed class EditCrmAuthorizationHandler
: AuthorizationHandler<EditCrmRequirement, Crm>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
EditCrmRequirement requirement,
Crm resource)
{
string? userIdValue =
context.User.FindFirstValue("sub") ??
context.User.FindFirstValue(
ClaimTypes.NameIdentifier);
string? tenantIdValue =
context.User.FindFirstValue("tenant_id");
if (!int.TryParse(userIdValue, out int userId) ||
!int.TryParse(tenantIdValue, out int tenantId))
{
return Task.CompletedTask;
}
if (resource.TenantId != tenantId)
{
return Task.CompletedTask;
}
bool canUpdateAll =
context.User.HasClaim(
"permission",
"crm.update.all");
bool canUpdateOwn =
context.User.HasClaim(
"permission",
"crm.update.own");
if (canUpdateAll)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
if (canUpdateOwn &&
resource.AssignedUserId == userId)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
public sealed class EditCrmAuthorizationHandler
: AuthorizationHandler<EditCrmRequirement, Crm>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
EditCrmRequirement requirement,
Crm resource)
{
string? userIdValue =
context.User.FindFirstValue("sub") ??
context.User.FindFirstValue(
ClaimTypes.NameIdentifier);
string? tenantIdValue =
context.User.FindFirstValue("tenant_id");
if (!int.TryParse(userIdValue, out int userId) ||
!int.TryParse(tenantIdValue, out int tenantId))
{
return Task.CompletedTask;
}
if (resource.TenantId != tenantId)
{
return Task.CompletedTask;
}
bool canUpdateAll =
context.User.HasClaim(
"permission",
"crm.update.all");
bool canUpdateOwn =
context.User.HasClaim(
"permission",
"crm.update.own");
if (canUpdateAll)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
if (canUpdateOwn &&
resource.AssignedUserId == userId)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}DI kaydı:
builder.Services.AddScoped<
IAuthorizationHandler,
EditCrmAuthorizationHandler>();builder.Services.AddScoped<
IAuthorizationHandler,
EditCrmAuthorizationHandler>();Burada şu kurallar uygulanıyor:
- Kullanıcı ID'si token'dan okunuyor.
- Tenant ID token'dan okunuyor.
- Kaydın tenant'ı ile kullanıcının tenant'ı karşılaştırılıyor.
crm.update.allpermission'ı olan kullanıcı bütün tenant kayıtlarını düzenleyebiliyor.crm.update.ownpermission'ı olan kullanıcı yalnızca kendisine atanmış kaydı düzenleyebiliyor.
10. Controller İçerisinde Resource Authorization
[Authorize]
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(
int id,
UpdateCrmRequest request,
[FromServices] IAuthorizationService authorizationService,
CancellationToken cancellationToken)
{
int tenantId = User.GetRequiredTenantId();
var crm = await _db.Crms
.SingleOrDefaultAsync(
x =>
x.Id == id &&
x.TenantId == tenantId,
cancellationToken);
if (crm is null)
return NotFound();
AuthorizationResult authorizationResult =
await authorizationService.AuthorizeAsync(
User,
crm,
new EditCrmRequirement());
if (!authorizationResult.Succeeded)
return Forbid();
crm.CompanyName = request.CompanyName;
crm.Phone = request.Phone;
crm.UpdatedAt = DateTimeOffset.UtcNow;
await _db.SaveChangesAsync(cancellationToken);
return NoContent();
}[Authorize]
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(
int id,
UpdateCrmRequest request,
[FromServices] IAuthorizationService authorizationService,
CancellationToken cancellationToken)
{
int tenantId = User.GetRequiredTenantId();
var crm = await _db.Crms
.SingleOrDefaultAsync(
x =>
x.Id == id &&
x.TenantId == tenantId,
cancellationToken);
if (crm is null)
return NotFound();
AuthorizationResult authorizationResult =
await authorizationService.AuthorizeAsync(
User,
crm,
new EditCrmRequirement());
if (!authorizationResult.Succeeded)
return Forbid();
crm.CompanyName = request.CompanyName;
crm.Phone = request.Phone;
crm.UpdatedAt = DateTimeOffset.UtcNow;
await _db.SaveChangesAsync(cancellationToken);
return NoContent();
}Burada iki farklı güvenlik katmanı bulunuyor.
Birinci katman: Tenant filtresi
x.TenantId == tenantIdx.TenantId == tenantIdBaşka tenant'a ait kayıt uygulama belleğine alınmıyor.
İkinci katman: Kayıt bazlı authorization
authorizationService.AuthorizeAsync(
User,
crm,
new EditCrmRequirement());authorizationService.AuthorizeAsync(
User,
crm,
new EditCrmRequirement());Kullanıcının bu belirli CRM kaydını düzenleme hakkı kontrol ediliyor.
Route-level authorization işlemi korur. Resource-based authorization ise belirli kaydı korur.
11. Yetkilendirmeyi Veritabanı Sorgusuna Taşımak
Listeleme işlemlerinde bütün kayıtları çekip daha sonra bellekte filtrelemek doğru değildir.
Yanlış:
var allRecords = await _db.Crms
.ToListAsync(cancellationToken);
var visibleRecords = allRecords
.Where(x => x.AssignedUserId == currentUserId)
.ToList();var allRecords = await _db.Crms
.ToListAsync(cancellationToken);
var visibleRecords = allRecords
.Where(x => x.AssignedUserId == currentUserId)
.ToList();Bu yaklaşım:
- Gereksiz veri getirir.
- Bellek kullanımını artırır.
- Yanlış bir dönüş işleminde yetkisiz verinin sızma riskini büyütür.
- Multi-tenant sistemlerde ciddi sonuçlar doğurabilir.
Daha doğru yaklaşım:
IQueryable<Crm> query = _db.Crms
.AsNoTracking()
.Where(x => x.TenantId == currentTenantId);
bool canReadAll = User.HasClaim(
"permission",
Permissions.CrmReadAll);
if (!canReadAll)
{
query = query.Where(
x => x.AssignedUserId == currentUserId);
}
var records = await query
.OrderByDescending(x => x.CreatedAt)
.Select(x => new CrmListItemDto
{
Id = x.Id,
CompanyName = x.CompanyName,
AssignedUserId = x.AssignedUserId
})
.ToListAsync(cancellationToken);IQueryable<Crm> query = _db.Crms
.AsNoTracking()
.Where(x => x.TenantId == currentTenantId);
bool canReadAll = User.HasClaim(
"permission",
Permissions.CrmReadAll);
if (!canReadAll)
{
query = query.Where(
x => x.AssignedUserId == currentUserId);
}
var records = await query
.OrderByDescending(x => x.CreatedAt)
.Select(x => new CrmListItemDto
{
Id = x.Id,
CompanyName = x.CompanyName,
AssignedUserId = x.AssignedUserId
})
.ToListAsync(cancellationToken);Yetki sınırı mümkün olduğunca sorgunun içine uygulanmalıdır.
Böylece SQL Server yalnızca kullanıcının görmesine izin verilen kayıtları döndürür.
12. Tenant ID Request Body'den Alınmamalıdır
Şu request tehlikelidir:
{
"companyName": "Example Company",
"tenantId": 9,
"createdByUserId": 125
}{
"companyName": "Example Company",
"tenantId": 9,
"createdByUserId": 125
}Kullanıcı tarayıcı geliştirici araçlarından request body'yi değiştirebilir:
{
"companyName": "Example Company",
"tenantId": 1,
"createdByUserId": 1
}{
"companyName": "Example Company",
"tenantId": 1,
"createdByUserId": 1
}Backend aşağıdaki alanları istemciden güvenilir kabul etmemelidir:
TenantIdCreatedByUserIdUpdatedByUserIdOwnerUserId- Yetki belirleyen role veya permission değerleri
Bunlar authenticated kullanıcının güvenilir server-side context'inden alınmalıdır:
int tenantId = User.GetRequiredTenantId();
int userId = User.GetRequiredUserId();
var crm = new Crm
{
CompanyName = request.CompanyName,
TenantId = tenantId,
CreatedByUserId = userId
};int tenantId = User.GetRequiredTenantId();
int userId = User.GetRequiredUserId();
var crm = new Crm
{
CompanyName = request.CompanyName,
TenantId = tenantId,
CreatedByUserId = userId
};İstemci, kendisinin kim olduğunu veya hangi tenant'a ait olduğunu belirleyemez.
13. React Tarafındaki Yetki Kontrolü Güvenlik Sağlar mı?
React'ta şu kontrol yapılabilir:
const canDelete =
currentUser.permissions.includes("crm.delete");
return (
<>
{canDelete && (
<button onClick={handleDelete}>
Kaydı Sil
</button>
)}
</>
);const canDelete =
currentUser.permissions.includes("crm.delete");
return (
<>
{canDelete && (
<button onClick={handleDelete}>
Kaydı Sil
</button>
)}
</>
);Bu kontrol faydalıdır.
Yetkisi olmayan kullanıcı gereksiz bir buton görmez. Kullanıcı deneyimi iyileşir.
Ancak saldırgan:
- React state'ini değiştirebilir.
- JavaScript fonksiyonunu doğrudan çağırabilir.
- İsteği Postman ile gönderebilir.
- Tarayıcı konsolundan
fetchçalıştırabilir. - UI'daki gizli elementi görünür hâle getirebilir.
Client tarafında çalışan kod kullanıcı tarafından değiştirilebilir. Microsoft da istemci tarafındaki authorization kontrollerinin güvenlik kuralını mutlak şekilde uygulamak için güvenilir kabul edilemeyeceğini belirtir.
Bu nedenle:
{canDelete && <DeleteButton />}{canDelete && <DeleteButton />}yalnızca kullanıcı deneyimidir.
Gerçek güvenlik:
[Authorize(Policy = Permissions.CrmDelete)][Authorize(Policy = Permissions.CrmDelete)]ve gerekiyorsa resource-level kontroldür.
Frontend yetkiyi gösterir. Backend yetkiyi uygular.
14. UI Bypass Saldırısı
React uygulamasında silme butonunun gizli olduğunu düşünelim.
Saldırgan doğrudan şu isteği gönderebilir:
fetch("/api/v1/crm/125", {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`
}
});fetch("/api/v1/crm/125", {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`
}
});Backend yalnızca [Authorize] kontrolü yapıyorsa istek başarılı olabilir.
Doğru endpoint:
[Authorize(Policy = Permissions.CrmDelete)]
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(
int id,
CancellationToken cancellationToken)
{
int tenantId = User.GetRequiredTenantId();
var crm = await _db.Crms
.SingleOrDefaultAsync(
x =>
x.Id == id &&
x.TenantId == tenantId,
cancellationToken);
if (crm is null)
return NotFound();
_db.Crms.Remove(crm);
await _db.SaveChangesAsync(cancellationToken);
return NoContent();
}[Authorize(Policy = Permissions.CrmDelete)]
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(
int id,
CancellationToken cancellationToken)
{
int tenantId = User.GetRequiredTenantId();
var crm = await _db.Crms
.SingleOrDefaultAsync(
x =>
x.Id == id &&
x.TenantId == tenantId,
cancellationToken);
if (crm is null)
return NotFound();
_db.Crms.Remove(crm);
await _db.SaveChangesAsync(cancellationToken);
return NoContent();
}Saldırgan butonu görünür yapabilir.
Ancak backend permission'ı kontrol ettiği sürece işlemi gerçekleştiremez.
15. IDOR Saldırısı
Kullanıcı kendisine ait kaydı görüntülüyor:
GET /api/v1/crm/100GET /api/v1/crm/100Daha sonra URL'deki ID'yi değiştiriyor:
GET /api/v1/crm/101GET /api/v1/crm/101Eğer backend yalnızca kullanıcının giriş yapmış olup olmadığını kontrol ediyorsa başka bir kullanıcının kaydı dönebilir.
Yanlış:
[Authorize]
[HttpGet("{id:int}")]
public async Task<IActionResult> Get(int id)
{
var crm = await _db.Crms.FindAsync(id);
return crm is null
? NotFound()
: Ok(crm);
}[Authorize]
[HttpGet("{id:int}")]
public async Task<IActionResult> Get(int id)
{
var crm = await _db.Crms.FindAsync(id);
return crm is null
? NotFound()
: Ok(crm);
}Daha güvenli:
[Authorize(Policy = Permissions.CrmRead)]
[HttpGet("{id:int}")]
public async Task<IActionResult> Get(
int id,
CancellationToken cancellationToken)
{
int userId = User.GetRequiredUserId();
int tenantId = User.GetRequiredTenantId();
bool canReadAll = User.HasClaim(
"permission",
Permissions.CrmReadAll);
IQueryable<Crm> query = _db.Crms
.AsNoTracking()
.Where(x =>
x.Id == id &&
x.TenantId == tenantId);
if (!canReadAll)
{
query = query.Where(
x => x.AssignedUserId == userId);
}
var crm = await query
.Select(x => new CrmDetailDto
{
Id = x.Id,
CompanyName = x.CompanyName,
Phone = x.Phone
})
.SingleOrDefaultAsync(cancellationToken);
return crm is null
? NotFound()
: Ok(crm);
}[Authorize(Policy = Permissions.CrmRead)]
[HttpGet("{id:int}")]
public async Task<IActionResult> Get(
int id,
CancellationToken cancellationToken)
{
int userId = User.GetRequiredUserId();
int tenantId = User.GetRequiredTenantId();
bool canReadAll = User.HasClaim(
"permission",
Permissions.CrmReadAll);
IQueryable<Crm> query = _db.Crms
.AsNoTracking()
.Where(x =>
x.Id == id &&
x.TenantId == tenantId);
if (!canReadAll)
{
query = query.Where(
x => x.AssignedUserId == userId);
}
var crm = await query
.Select(x => new CrmDetailDto
{
Id = x.Id,
CompanyName = x.CompanyName,
Phone = x.Phone
})
.SingleOrDefaultAsync(cancellationToken);
return crm is null
? NotFound()
: Ok(crm);
}Kullanıcı yetkili olmadığı kaydın ID'sini denediğinde kayıt sorgudan dönmez.
16. Neden Bazı Durumlarda 404 Döndürülür?
Kullanıcı giriş yapmış ancak kayda erişim yetkisine sahip değilse teorik olarak 403 Forbidden döndürülebilir.
Ancak şu cevap:
403 Forbidden403 Forbiddenkaynağın sistemde mevcut olduğunu doğrulayabilir.
Bazı sistemler yetkisiz kullanıcılara şu cevabı verir:
404 Not Found404 Not FoundBöylece kaydın gerçekten bulunup bulunmadığı hakkında bilgi verilmez.
Bu her endpoint için zorunlu bir kural değildir. Uygulamanın API sözleşmesine göre karar verilmelidir.
Genel yaklaşım:
- Genel permission yoksa:
403 Forbidden - Kullanıcı belirli kaydı göremiyorsa: Gereksinime göre
404 Not Found - Authentication yoksa:
401 Unauthorized
17. Middleware Sırası
Authentication ve authorization middleware sırası önemlidir:
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();Authentication middleware önce çalışarak HttpContext.User değerini oluşturur.
Authorization middleware daha sonra bu kullanıcı üzerinden policy kontrollerini yapar.
Microsoft dokümantasyonu CORS, authentication ve authorization middleware'lerinin bu sırayla yerleştirilmesi gerektiğini belirtir. Middleware sıralaması güvenlik ve uygulama davranışı üzerinde doğrudan etkilidir.
Yanlış sıra:
app.UseAuthorization();
app.UseAuthentication();app.UseAuthorization();
app.UseAuthentication();Authorization çalıştığında kullanıcı henüz oluşturulmamış olabilir.
18. Deny by Default Yaklaşımı
Bir developer yeni endpoint oluşturduğunda [Authorize] eklemeyi unutabilir.
Bunu azaltmak için bütün endpoint'lerde varsayılan olarak authentication zorunlu tutulabilir:
using Microsoft.AspNetCore.Authorization;
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy =
new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});using Microsoft.AspNetCore.Authorization;
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy =
new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});Artık authorization metadata'sı bulunmayan endpoint'lerde bile kullanıcıdan authentication istenir.
Public endpoint'ler açıkça belirtilir:
[AllowAnonymous]
[HttpPost("login")]
public async Task<IActionResult> Login(...)
{
// ...
}[AllowAnonymous]
[HttpPost("login")]
public async Task<IActionResult> Login(...)
{
// ...
}Bu yaklaşım şu prensibi uygular:
Her şey varsayılan olarak kapalıdır. Yalnızca açıkça belirtilen alanlar erişime açılır.
Fallback policy, authorization kuralı belirtilmeyen isteklere uygulanabilir; bu nedenle static file ve middleware sıralaması da ayrıca değerlendirilmelidir.
19. JWT İçindeki Permission'lar Ne Zaman Güncellenir?
Kullanıcının JWT'sinde şu permission bulunduğunu düşünelim:
{
"permission": "crm.delete",
"exp": 1785830400
}{
"permission": "crm.delete",
"exp": 1785830400
}Veritabanında kullanıcının silme yetkisi kaldırıldığında mevcut JWT değişmez.
Token süresi dolana kadar eski permission geçerli kalabilir.
Bu problem için şu yaklaşımlar kullanılabilir:
Kısa access token süresi
Access token kısa süre içinde sona erer. Yeni token oluşturulurken güncel permission'lar alınır.
Security version
JWT içine bir sürüm yazılır:
{
"security_version": "5"
}{
"security_version": "5"
}Kritik bir rol veya permission değişikliğinde veritabanındaki sürüm artırılır:
security_version = 6security_version = 6Yüksek riskli endpoint'lerde token sürümü ile güncel sürüm karşılaştırılabilir.
Permission'ları sunucu tarafında çözümlemek
JWT yalnızca kullanıcı ve session kimliğini taşır. Permission'lar cache veya veritabanından alınır.
Bu, anlık yetki değişikliği sağlar ancak daha fazla sunucu işlemi gerektirir.
Token tabanlı authorization tasarımında performans, token boyutu ve anlık permission iptali birlikte değerlendirilmelidir.
20. Authorization Testleri
Authorization yalnızca manuel olarak denenmemelidir.
Her kritik endpoint için en az şu senaryolar test edilmelidir:
Authentication yok
Beklenen sonuç: 401Beklenen sonuç: 401Kullanıcı giriş yapmış ancak permission yok
Beklenen sonuç: 403Beklenen sonuç: 403Permission var ve kayıt kullanıcıya atanmış
Beklenen sonuç: 200 veya 204Beklenen sonuç: 200 veya 204Permission var ancak kayıt başka kullanıcıya atanmış
Beklenen sonuç: 403 veya 404Beklenen sonuç: 403 veya 404Kayıt başka tenant'a ait
Beklenen sonuç: 404Beklenen sonuç: 404Request body içindeki TenantId değiştirildi
Beklenen sonuç:
Backend body içindeki TenantId değerini dikkate almamalı.Beklenen sonuç:
Backend body içindeki TenantId değerini dikkate almamalı.React tarafındaki role değiştirildi
Beklenen sonuç:
Backend kararı etkilenmemeli.Beklenen sonuç:
Backend kararı etkilenmemeli.Basit bir integration test görünümü:
[Fact]
public async Task Delete_without_permission_returns_forbidden()
{
using HttpClient client =
_factory.CreateClient();
client.SetAuthenticatedUser(
userId: 42,
tenantId: 7,
permissions: [Permissions.CrmRead]);
HttpResponseMessage response =
await client.DeleteAsync("/api/v1/crm/100");
Assert.Equal(
HttpStatusCode.Forbidden,
response.StatusCode);
}[Fact]
public async Task Delete_without_permission_returns_forbidden()
{
using HttpClient client =
_factory.CreateClient();
client.SetAuthenticatedUser(
userId: 42,
tenantId: 7,
permissions: [Permissions.CrmRead]);
HttpResponseMessage response =
await client.DeleteAsync("/api/v1/crm/100");
Assert.Equal(
HttpStatusCode.Forbidden,
response.StatusCode);
}Bir authorization sistemi, yetkili kullanıcıların başarılı olmasını test ettiği kadar yetkisiz kullanıcıların başarısız olmasını da test etmelidir.
21. Sık Yapılan Hatalar
Yalnızca [Authorize] kullanmak
Bütün authenticated kullanıcıların endpoint'e erişmesine neden olabilir.
Yetkiyi yalnızca React tarafında kontrol etmek
UI değiştirilebilir ve API'ye doğrudan istek gönderilebilir.
Her endpoint'e role isimleri yazmak
Role değişikliklerini ve yeni görev tanımlarını yönetmeyi zorlaştırır.
Tenant ID'yi request body'den almak
Kullanıcı başka tenant adına işlem yapmaya çalışabilir.
Kayıt sahibini istemciden almak
CreatedByUserId veya OwnerId değiştirilerek yetki yükseltme denenebilir.
Bütün kayıtları getirip bellekte filtrelemek
Gereksiz veri yükler ve yetkisiz verinin uygulama katmanına taşınmasına neden olur.
Resource authorization yapmamak
Kullanıcı permission'a sahip olsa bile başka kullanıcıların kayıtlarına erişebilir.
Permission değişikliklerinin JWT'ye anında yansıyacağını düşünmek
Mevcut token süresi dolana kadar eski claim'ler geçerli olabilir.
Authentication ve authorization middleware sırasını karıştırmak
Policy kontrollerinin beklenmeyen şekilde çalışmasına neden olabilir.
Authorization hatalarını loglamamak
ID değiştirme, başka tenant'a erişim veya tekrarlanan yasaklı işlemler fark edilmeyebilir.
22. Production Kontrol Listesi
Production'a çıkmadan önce şu sorular cevaplanmalıdır:
- Bütün private endpoint'ler authentication istiyor mu?
- Fallback policy uygulanıyor mu?
- Endpoint'ler role yerine permission üzerinden korunuyor mu?
- Permission isimleri merkezi olarak tanımlanmış mı?
- Tenant ID güvenilir kullanıcı context'inden alınıyor mu?
- User ID request body'den alınmıyor mu?
- Liste sorgularında kayıt erişimi SQL seviyesinde filtreleniyor mu?
- Kayıt bazlı işlemlerde resource authorization uygulanıyor mu?
- Frontend kontrolleri backend'de yeniden doğrulanıyor mu?
- Başka kullanıcıya ait ID denemeleri engelleniyor mu?
- Başka tenant'a ait kayıtlar uygulama belleğine alınmıyor mu?
- 401, 403 ve 404 cevapları tutarlı mı?
- JWT içindeki permission'ların güncellik stratejisi var mı?
- Kritik permission değişikliklerinde session iptal edilebiliyor mu?
- Authorization başarısızlıkları güvenli biçimde loglanıyor mu?
- Yetkisiz senaryolar integration testlerle doğrulanıyor mu?
UseAuthenticationileUseAuthorizationsırası doğru mu?- Public endpoint'ler yalnızca açıkça
[AllowAnonymous]ile belirtiliyor mu?
Sonuç
Güvenli bir ASP.NET Core uygulamasında authorization yalnızca controller'a [Authorize] yazmaktan ibaret değildir.
Sağlıklı bir model şu katmanlardan oluşur:
Authentication
↓
ClaimsPrincipal
↓
Permission Policy
↓
Resource-Based Authorization
↓
Tenant ve kayıt filtreleri
↓
İşlemAuthentication
↓
ClaimsPrincipal
↓
Permission Policy
↓
Resource-Based Authorization
↓
Tenant ve kayıt filtreleri
↓
İşlemEn önemli kurallar şunlardır:
Kullanıcının giriş yapmış olması, işlemi yapmaya yetkili olduğu anlamına gelmez.
Role organizasyonel bir gruptur; permission belirli bir işlemi ifade eder.
Frontend authorization güvenlik sağlamaz, yalnızca kullanıcı deneyimini düzenler.
Gerçek yetki kontrolü her zaman backend'de yapılmalıdır.
Route-level authorization işlemi, resource-based authorization ise belirli kaydı korur.
Tenant ve kullanıcı bilgileri request body'den değil, güvenilir authentication context'inden alınmalıdır.
Yetkisiz veri mümkün olduğunca veritabanı sorgusundan hiç dönmemelidir.
JWT bize kullanıcının kimliğini taşır.
Policy kullanıcının genel olarak hangi işlemleri yapabileceğini belirler.
Resource-based authorization ise bu kullanıcının hangi kayıt üzerinde işlem yapabileceğini belirler.
Senior seviyede güvenli authorization mimarisi, bu üç seviyeyi birbirinden ayırarak birlikte uygulayabilmektir.