Cum construiești permisiuni dinamice și multi-tenant în ASP.NET Core
Seria: Security by Design în .NET: De la JWT la Certificate-based Authentication
În articolele anterioare am discutat despre:
-
JWT
-
Refresh Tokens
-
Authorization (roles vs policies)
Dar apare o problemă reală în aplicații:
JWT-ul conține informații limitate și statice.
Ce faci când ai nevoie de:
-
permisiuni dinamice din DB
-
logică multi-tenant
-
roluri care se schimbă fără re-login
Aici intervine Claims Transformation.
🧠 Ce este Claims Transformation
Claims Transformation îți permite să:
modifici sau să adaugi claims după autentificare, la fiecare request.
ASP.NET Core oferă interfața:
IClaimsTransformation
Este executată după validarea token-ului.
⚙️ 1️⃣ Implementare IClaimsTransformation
Exemplu simplu:
public class CustomClaimsTransformation : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var identity = (ClaimsIdentity)principal.Identity;
if (!identity.HasClaim(c => c.Type == "custom"))
{
identity.AddClaim(new Claim("custom", "true"));
}
return Task.FromResult(principal);
}
}
Înregistrare:
builder.Services.AddScoped<IClaimsTransformation, CustomClaimsTransformation>();
🧩 2️⃣ Enriching Claims din baza de date
Cel mai util scenariu:
adaugi claims din DB la fiecare request.
Exemplu:
public class DbClaimsTransformation : IClaimsTransformation
{
private readonly IUserRepository _repo;
public DbClaimsTransformation(IUserRepository repo)
{
_repo = repo;
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var identity = (ClaimsIdentity)principal.Identity;
var userId = identity.FindFirst("sub")?.Value;
var user = await _repo.GetUserWithPermissions(userId);
foreach (var permission in user.Permissions)
{
identity.AddClaim(new Claim("permission", permission));
}
return principal;
}
}
🏢 3️⃣ Multi-tenant logic
În aplicațiile reale:
un user poate aparține mai multor companii.
JWT-ul conține:
{
"sub": "user123"
}
Dar la runtime ai nevoie de:
-
CompanyId curent
-
Rol în companie
-
Permisiuni specifice companiei
Claims Transformation poate face asta:
identity.AddClaim(new Claim("companyId", selectedCompanyId));
identity.AddClaim(new Claim("role", "Manager"));
🔄 4️⃣ Dynamic Permissions
Role-based nu este suficient în aplicații complexe.
În schimb:
permission = "invoice.read"
permission = "invoice.create"
permission = "invoice.delete"
Adăugate ca claims:
identity.AddClaim(new Claim("permission", "invoice.read"));
Apoi folosești policy:
options.AddPolicy("CanReadInvoice",
policy => policy.RequireClaim("permission", "invoice.read"));
⚠️ Probleme și capcane
❌ Executare la fiecare request
Claims Transformation rulează la fiecare request.
Dacă faci query în DB:
→ impact de performanță
✅ Soluții
-
cache (MemoryCache / Redis)
-
claim versioning
-
limitare date
❌ Duplicate claims
Dacă nu verifici:
→ vei adăuga aceleași claims de mai multe ori
❌ Logică prea complexă
Nu transforma ClaimsTransformation într-un „mini service layer”.
🧱 Arhitectură recomandată
JWT conține:
sub (userId)
email
Claims Transformation adaugă:
permissions
companyId
roles
Authorization folosește:
policies + handlers
🔥 Best Practices
✔ JWT minimal (doar identificare)
✔ Claims enrichment din DB
✔ Cache pentru performanță
✔ Dynamic permissions
✔ Multi-tenant aware
✔ Policy-based authorization
🎯 Concluzie
Claims Transformation este cheia pentru:
✔ permisiuni dinamice
✔ aplicații multi-tenant
✔ logică flexibilă de autorizare
Este diferența dintre:
un sistem simplu de roluri
și
un sistem enterprise de permisiuni