Advanced Authentication & Authorization
Beyond Basic Roles
In complex enterprise applications, simple Role-Based Access Control (RBAC) often becomes unmanageable (“Role Explosion”). Modern security in .NET favors Policy-Based Authorization and Claims Transformation.
1. Claims Transformation
Authentication provides the Who, but authorization often needs the What Else. Claims transformation occurs after a user is authenticated but before they hit your business logic. It allows you to augment the ClaimsPrincipal with data from external sources (e.g., a database or a legacy LDAP system).
public class MyClaimsTransformation : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
// Add custom application-specific claims without modifying the Identity Provider
var identity = (ClaimsIdentity)principal.Identity;
identity.AddClaim(new Claim("SubscriptionLevel", "Premium"));
return Task.FromResult(principal);
}
}
2. Policy-Based Authorization (ABAC)
Instead of hardcoding [Authorize(Roles = "Manager")], you define Policies. A policy is a collection of Requirements and Handlers. This approach follows Attribute-Based Access Control (ABAC).
Key Components
- Requirement: A data class that defines the parameters (e.g.,
MinimumAgeRequirement). - Handler: The logic that evaluates if the requirement is met based on the user’s claims.
3. Federated Identity & OIDC
By using OpenID Connect (OIDC), you offload the risk of storing passwords to trusted Identity Providers (IdPs) like Azure AD, Auth0, or Google. Your application simply receives a signed JWT (JSON Web Token) that proves the user’s identity.
Practice Exercise
Implement a custom authorization handler that checks if a user has “Veteran” status based on a RegistrationDate claim (must be > 1 year).
Answer
1. The Requirement
public class VeteranRequirement : IAuthorizationRequirement { }
2. The Handler
public class VeteranHandler : AuthorizationHandler<VeteranRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, VeteranRequirement requirement)
{
var regDateClaim = context.User.FindFirst(c => c.Type == "RegistrationDate")?.Value;
if (DateTime.TryParse(regDateClaim, out var regDate))
{
if (regDate.AddYears(1) <= DateTime.UtcNow)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
3. Registration in Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("IsVeteran", policy => policy.Requirements.Add(new VeteranRequirement()));
});
builder.Services.AddSingleton<IAuthorizationHandler, VeteranHandler>();
4. Usage in Controller
[Authorize(Policy = "IsVeteran")]
public IActionResult GetExclusiveOffer() => Ok("Special code for veterans: VET-2025");
Why This Architecture Works
- Decoupling: The Controller doesn’t care how someone becomes a veteran; it just knows it needs that policy satisfied.
- Scalability: If the business rule changes to “2 years,” you only change one line in the
VeteranHandler, not every controller in the app. - Security: Claims are cryptographically signed in the JWT, meaning they cannot be tampered with by the client.
Summary
Advanced identity management is about externalizing rules. By moving your security logic into Policies and Claims Managers, you keep your business code clean, maintainable, and highly resilient to changing compliance requirements.