In the realm of web application security, ensuring that users have the correct permissions to access various resources and perform specific actions is crucial. While authentication verifies the identity of a user, authorization determines what an authenticated user is allowed to do. In the context of .NET, implementing permission-based authentication and authorization using cookies is a powerful approach to manage user access and maintain a secure application environment.
Permission-based authentication and authorization in .NET involves assigning permissions to users or roles and verifying these permissions before granting access to certain parts of the application. Using cookies for this process enhances the security and efficiency of maintaining user sessions and permissions.
In this article, we will delve into the implementation of permission-based authentication and authorization in a .NET web application using cookies. We will explore how to configure authentication schemes, define and enforce permissions, and ensure that users have the appropriate access levels.
Click to see Project Source Code
Required Packages:
To implement permission-based authentication and authorization in a .NET application, it's essential to set up your identity models correctly. These models will represent your users and roles, and they will be integral to managing authentication and authorization within your application.
Let's first define Permission model with predefined permissions. Here, a factory method is used to create an instance of Permission to encapsulate the creation (in this case default private constructor is needed for EF Core). Throughout the application code, this clean code convention is used in several places.
// Permission Model
public sealed class Permission
{
public int Id { get; }
public string Name { get; }
public byte[]? RowVersion { get; }
private Permission(string name)
{
Name = name;
}
private Permission()
{ }
public static Permission Create(string name)
{
return new Permission(name);
}
}
// Predefined Permissions (followed GitHub permission naming conventions)
public static class Permissions
{
public const string RoleView = "role:view";
public const string RoleCreate = "role:create";
public const string RoleUpdate = "role:update";
public const string RoleDelete = "role:delete";
public const string UserView = "role:view";
public const string UserCreate = "role:create";
public const string UserUpdate = "user:update";
public const string UserDelete = "user:delete";
public const string PermissionView = "permission:view";
public const string PermissionCreate = "permission:create";
public const string PermissionUpdate = "permission:update";
public const string PermissionDelete = "permission:delete";
public const string AccessControlManage = "accesscontrol:manage";
public static string[] All()
{
return
[
RoleView,
RoleCreate,
RoleUpdate,
RoleDelete,
UserView,
UserCreate,
UserUpdate,
UserDelete,
PermissionView,
PermissionCreate,
PermissionUpdate,
PermissionDelete,
AccessControlManage
];
}
}
Now, let's move into overriding default Identity models. Our models extend the built-in Identity models provided by ASP.NET Core Identity (https://learn.microsoft.com/en-us/aspnet/core/security/authentication/customize-identity-model?view=aspnetcore-8.0#add-all-navigation-properties).
public class ApplicationRole : IdentityRole<Guid>
{
public ICollection<ApplicationUserRole> UserRoles { get; } = [];
public ICollection<ApplicationRoleClaim> RoleClaims { get; } = [];
public ICollection<Permission> Permissions { get; } = [];
public void AddPermission(Permission permission)
{
Permissions.Add(permission);
}
public void RemovePermission(Permission permission)
{
Permissions.Remove(permission);
}
public void AddPermissions(IEnumerable<Permission> permissions)
{
foreach (var permission in permissions)
{
AddPermission(permission);
}
}
}
// Predefined roles
public static class ApplicationRoles
{
public const string Administrator = "Administrator";
public const string Visitor = "Visitor";
public static IEnumerable<string> All()
{
yield return Administrator;
yield return Visitor;
}
}
public class ApplicationRoleClaim : IdentityRoleClaim<Guid>
{
public ApplicationRole Role { get; }
}
// Intermediate model between Permission and ApplicationRole,
// as the relationship will be Many-to-Many
public class ApplicationRolePermission
{
public Guid ApplicationRoleId { get; set; }
public int PermissionId { get; set; }
}
public class ApplicationUser : IdentityUser<Guid>
{
public ICollection<ApplicationUserClaim> Claims { get; } = [];
public ICollection<ApplicationUserLogin> Logins { get; } = [];
public ICollection<ApplicationUserToken> Tokens { get; } = [];
public ICollection<ApplicationUserRole> UserRoles { get; } = [];
private ApplicationUser() { }
public static ApplicationUser Create()
{
return new ApplicationUser();
}
}
public class ApplicationUserClaim : IdentityUserClaim<Guid>
{
public ApplicationUser User { get; }
}
public class ApplicationUserLogin : IdentityUserLogin<Guid>
{
public ApplicationUser User { get; }
}
public class ApplicationUserRole : IdentityUserRole<Guid>
{
public ApplicationUser User { get; }
public ApplicationRole Role { get; }
}
public class ApplicationUserToken : IdentityUserToken<Guid>
{
public ApplicationUser User { get; }
}
Models are then configured for EF Core
public class ApplicationRoleClaimConfiguration : IEntityTypeConfiguration<ApplicationRoleClaim>
{
private const string _tableName = "ApplicationRoleClaims";
public void Configure(EntityTypeBuilder<ApplicationRoleClaim> builder)
{
builder.ToTable(_tableName);
}
}
public class ApplicationRoleConfiguration : IEntityTypeConfiguration<ApplicationRole>
{
private const string _tableName = "ApplicationRoles";
public void Configure(EntityTypeBuilder<ApplicationRole> builder)
{
builder.ToTable(_tableName);
builder.HasMany(r => r.UserRoles)
.WithOne()
.HasForeignKey(ur => ur.RoleId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(r => r.RoleClaims)
.WithOne()
.HasForeignKey(rc => rc.RoleId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(r => r.Permissions)
.WithMany()
.UsingEntity<ApplicationRolePermission>();
}
}
public class ApplicationRolePermissionConfiguration : IEntityTypeConfiguration<ApplicationRolePermission>
{
private const string _tableName = "ApplicationRolePermission";
public void Configure(EntityTypeBuilder<ApplicationRolePermission> builder)
{
builder.ToTable(_tableName);
builder.HasKey(rp => new { rp.ApplicationRoleId, rp.PermissionId });
}
}
public class ApplicationUserClaimConfiguration : IEntityTypeConfiguration<ApplicationUserClaim>
{
private const string _tableName = "ApplicationUserClaims";
public void Configure(EntityTypeBuilder<ApplicationUserClaim> builder)
{
builder.ToTable(_tableName);
}
}
public class ApplicationUserConfiguration : IEntityTypeConfiguration<ApplicationUser>
{
private const string _tableName = "ApplicationUsers";
public void Configure(EntityTypeBuilder<ApplicationUser> builder)
{
builder.ToTable(_tableName);
builder.HasMany(u => u.Claims)
.WithOne()
.HasForeignKey(c => c.UserId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(u => u.Logins)
.WithOne()
.HasForeignKey(l => l.UserId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(u => u.Tokens)
.WithOne()
.HasForeignKey(t => t.UserId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(u => u.UserRoles)
.WithOne()
.HasForeignKey(ur => ur.UserId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
}
}
public class ApplicationUserLoginConfiguration : IEntityTypeConfiguration<ApplicationUserLogin>
{
private const string _tableName = "ApplicationUserLogins";
public void Configure(EntityTypeBuilder<ApplicationUserLogin> builder)
{
builder.ToTable(_tableName);
}
}
public class ApplicationUserRoleConfiguration : IEntityTypeConfiguration<ApplicationUserRole>
{
private const string _tableName = "ApplicationUserRoles";
public void Configure(EntityTypeBuilder<ApplicationUserRole> builder)
{
builder.ToTable(_tableName);
}
}
public class ApplicationUserTokenConfiguration : IEntityTypeConfiguration<ApplicationUserToken>
{
private const string _tableName = "ApplicationUserTokens";
public void Configure(EntityTypeBuilder<ApplicationUserToken> builder)
{
builder.ToTable(_tableName);
}
}
public class PermissionConfiguration : IEntityTypeConfiguration<Permission>
{
private const string _tableName = "Permissions";
public void Configure(EntityTypeBuilder<Permission> builder)
{
builder.ToTable(_tableName);
builder.HasKey(p => p.Id);
builder.Property(p => p.Name).IsRequired();
}
}
Now, it is time to define the database context. It extends the IdentityDbContext with the custom identity models as generic types. It also has a database schema to provide isolation. This database context will scan through the assembly of application (to get the current assembly created a static class AssemblyReference.cs
in the project root) and apply the entity configurations we defined before.
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: IdentityDbContext<ApplicationUser, ApplicationRole, Guid,
ApplicationUserClaim, ApplicationUserRole, ApplicationUserLogin,
ApplicationRoleClaim, ApplicationUserToken>(options)
{
public const string Schema = "Authentication";
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.HasDefaultSchema(Schema);
modelBuilder.ApplyConfigurationsFromAssembly(AssemblyReference.Assembly);
}
public DbSet<Permission> Permissions { get; set; } = null!;
}
In Program.cs file, I'm setting up the database context for our ASP.NET Core app using Entity Framework Core with PostgreSQL. I'm adding ApplicationDbContext
to the service container and specifying PostgreSQL as the provider. The connection string is pulled from the configuration, and I'm customizing the options to place the migration history table in a specific schema within the database.
builder.Services.AddDbContext<ApplicationDbContext>((options) =>
{
options.UseNpgsql(
builder.Configuration.GetConnectionString("Database"),
npgsqlOptionsAction => npgsqlOptionsAction.MigrationsHistoryTable(
HistoryRepository.DefaultTableName,
ApplicationDbContext.Schema));
});
Then we add Identity, Authentication, and Authorization respectively.
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// The default Cookie name for authentication, will be
// CookieAuthenticationDefaults.AuthenticationScheme
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
options.SlidingExpiration = true;
options.AccessDeniedPath = "/";
options.LoginPath = "/Authentication/Login/";
});
builder.Services.AddAuthorizationBuilder();
During app configuration, I am populating the database with predefined data.
if(app.Environment.IsDevelopment())
{
await app.SeedDatabaseAsync(app.Configuration);
}
// Data/DatabaseSeeder.cs
public static class DatabaseSeeder
{
public static async Task SeedDatabaseAsync(this IApplicationBuilder app, IConfiguration configuration)
{
using var scope = app.ApplicationServices.CreateScope();
var services = scope.ServiceProvider;
var dbContext = services.GetRequiredService<ApplicationDbContext>();
var roleManager = services.GetRequiredService<RoleManager<ApplicationRole>>();
var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
if (!await roleManager.Roles.AnyAsync())
{
foreach (var role in ApplicationRoles.All())
{
await roleManager.CreateAsync(new ApplicationRole
{
Name = role
});
}
await dbContext.Permissions.AddRangeAsync(Permissions.All().Select(p => Permission.Create(p)));
await dbContext.SaveChangesAsync();
var adminRole = await roleManager.FindByNameAsync(ApplicationRoles.Administrator);
if (adminRole is null)
{
return;
}
var permissions = await dbContext.Permissions.ToListAsync();
adminRole.AddPermissions(permissions);
await roleManager.UpdateAsync(adminRole);
var adminUser = ApplicationUser.Create();
await userManager.SetEmailAsync(adminUser, configuration["Admin:Email"]);
await userManager.SetUserNameAsync(adminUser, configuration["Admin:Username"]);
await userManager.CreateAsync(adminUser, configuration["Admin:Password"]!);
await userManager.AddToRoleAsync(adminUser, ApplicationRoles.Administrator);
}
}
}
To manage permissions within the application, you need a service that can handle the retrieval of permissions and the assignment of permissions to users and roles. The PermissionService
class serves this purpose. The PermissionService
class implements the IPermissionService
interface and provides methods to retrieve all permissions from the database and retrieve the permissions assigned to a specific user by checking the roles they belong to and the permissions associated with those roles.
public interface IPermissionService
{
Task<IEnumerable<Permission>> GetPermissionsAsync();
Task<IEnumerable<string>> GetPermissionsForUserAsync(Guid userId);
}
public class PermissionService : IPermissionService
{
private readonly ApplicationDbContext _dbContext;
private readonly RoleManager<ApplicationRole> _roleManager;
private readonly UserManager<ApplicationUser> _userManager;
public PermissionService(
ApplicationDbContext dbContext,
RoleManager<ApplicationRole> roleManager,
UserManager<ApplicationUser> userManager)
{
_dbContext = dbContext;
_roleManager = roleManager;
_userManager = userManager;
}
public async Task<IEnumerable<Permission>> GetPermissionsAsync()
{
return await _dbContext.Permissions.ToListAsync();
}
public async Task<IEnumerable<string>> GetPermissionsForUserAsync(Guid userId)
{
var user = await _userManager.FindByIdAsync(userId.ToString());
if (user == null)
{
return Array.Empty<string>();
}
var roleNames = await _userManager.GetRolesAsync(user);
return await _roleManager.Roles
.Include(r => r.Permissions)
.Where(r => roleNames.Contains(r.Name!))
.SelectMany(r => r.Permissions)
.Select(p => p.Name)
.Distinct()
.ToArrayAsync();
}
}
The purpose of the ApplicationClaimTypes
class is to centralize the definition of custom claim types in the application. It helps ensure consistency and reduces the likelihood of errors that can arise from using hard-coded string values throughout your codebase.
public class ApplicationClaimTypes
{
public const string Permission = "permission";
public const string Id = "id";
}
The ApplicationUserExtensions
class is a static class that provides extension methods for the ClaimsPrincipalsuch
as retrieving all permissions claims associated with the ClaimsPrincipal
(typically representing the current user) and retrieving the user's ID from the claims associated with the ClaimsPrincipal
.
public static class ApplicationUserExtensions
{
public static HashSet<string> GetPermissions(this ClaimsPrincipal claimsPrincipal)
{
return claimsPrincipal
.FindAll(ApplicationClaimTypes.Permission)
.Select(c => c.Value)
.ToHashSet();
}
public static Guid GetUserId(this ClaimsPrincipal claimsPrincipal)
{
var id = claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier)
?? throw new InvalidOperationException("User id claim not found.");
return Guid.Parse(id);
}
}
We need a way to dynamically add claims to a user's ClaimsPrincipal
based on their permissions. In this case PermissionClaimsTransformation
class which is an implementation of the IClaimsTransformation
interface will allow permissions to be managed and checked efficiently, without needing to store them in the database repeatedly or re-authenticate users. Permission will be fetched for the authenticated user and they will be added to ClaimsPrincipal
as custom claims.
public class PermissionClaimsTransformation(IServiceScopeFactory serviceScopeFactory)
: IClaimsTransformation
{
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
// ensure that the transformation is only applied to authenticated users
// who do not already have permission claims.
if (principal.Identity?.IsAuthenticated == true
&& !principal.HasClaim(c => c.Type == ApplicationClaimTypes.Permission))
{
using var scope = serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<IPermissionService>();
var permissions = await permissionService.GetPermissionsForUserAsync(principal.GetUserId());
var claims = permissions.Select(permission => new Claim(ApplicationClaimTypes.Permission, permission));
var claimsIdentity = new ClaimsIdentity();
claimsIdentity.AddClaims(claims);
principal.AddIdentity(claimsIdentity);
}
return principal;
}
}
// Registering the service in Program.cs
builder.Services.AddTransient<IClaimsTransformation,
PermissionClaimsTransformation>();
We define a custom authorization requirement PermissionAuthorizationRequirement
that represents a permission the user must have. In addition, we need PermissionAuthorizationHandler
implementing the logic to evaluate whether a user meets the PermissionAuthorizationRequirement
. It checks if the user is authenticated and has the required permission. If so, it marks the requirement as succeeded.
public class PermissionAuthorizationRequirement(string permission) : IAuthorizationRequirement
{
public string Permission { get; } = permission;
}
public class PermissionAuthorizationHandler
: AuthorizationHandler<PermissionAuthorizationRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionAuthorizationRequirement requirement)
{
if (context.User.Identity?.IsAuthenticated == true
&& context.User.GetPermissions().Contains(requirement.Permission))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
// Registering the service in Program.cs
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
The ApplicationAuthorizationPolicyProvider
class customizes the policy resolution process by dynamically creating authorization policies based on the permissions defined in the application. This approach allows for flexible and dynamic permission-based access control, where new permissions and associated policies can be handled without requiring changes to the application's static policy definitions. We first start by calling the base implementation to attempt to retrieve a predefined policy. If the base provider does not return a policy, we try to create a policy with a requirement if the given policy name exists inside the list of predefined Permissions.
public class ApplicationAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options)
: DefaultAuthorizationPolicyProvider(options)
{
public override async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
var policy = await base.GetPolicyAsync(policyName);
if (policy is null)
{
if (Permissions.All().Contains(policyName))
{
policy = new AuthorizationPolicyBuilder(CookieAuthenticationDefaults.AuthenticationScheme)
.AddRequirements(new PermissionAuthorizationRequirement(policyName))
.RequireAuthenticatedUser()
.Build();
}
}
return policy;
}
}
// Registering the service in Program.cs
builder.Services.AddSingleton<IAuthorizationPolicyProvider, ApplicationAuthorizationPolicyProvider>();
The AuthenticationController
class in this code snippet handles user authentication and registration within the application.
public AuthenticationController(
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
RoleManager<ApplicationRole> roleManager,
IPermissionService permissionService) : Controller
{
private readonly SignInManager<ApplicationUser> _signInManager = signInManager;
private readonly UserManager<ApplicationUser> _userManager = userManager;
private readonly RoleManager<ApplicationRole> _roleManager = roleManager;
private readonly IPermissionService _permissionService = permissionService;
}
Actions to sign in the user
[HttpGet("login")]
public IActionResult Login()
{
return View(new LoginViewModel { });
}
[HttpPost("login")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel request)
{
if (!ModelState.IsValid)
{
return View();
}
var user = await _userManager.FindByEmailAsync(request.Email);
if (user is null)
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View();
}
_signInManager.AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme;
var result = await _signInManager.PasswordSignInAsync(
user.UserName,
request.Password,
isPersistent: false,
lockoutOnFailure: false);
if (!result.Succeeded)
{
return View();
}
return RedirectToAction(nameof(Claims));
}
// Authentication/Login.cshtml
<form asp-controller="Authentication" asp-action="Login" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
<div class="form-group mb-3">
<label asp-for="Email" class="control-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Password" class="control-label"></label>
<input asp-for="Password" class="form-control" type="password" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<div class="form-group text-center mb-3">
<button type="submit" class="btn btn-primary">Login</button>
</div>
</form>
Actions to register the user and assign a role
[HttpGet("register")]
public IActionResult Register()
{
return View(new RegisterViewModel { });
}
[HttpPost("register")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel request)
{
if (!ModelState.IsValid)
{
return View();
}
var user = ApplicationUser.Create();
await _userManager.SetUserNameAsync(user, request.Email);
await _userManager.SetEmailAsync(user, request.Email);
var result = await _userManager.CreateAsync(user, request.Password);
if (!result.Succeeded)
{
return View();
}
var visitorRole = await _roleManager.FindByNameAsync(ApplicationRoles.Visitor);
if (visitorRole is not null)
{
await _userManager.AddToRoleAsync(user, visitorRole.Name);
}
return RedirectToAction(nameof(Login));
}
// Authentication/Register.cshtml
<form asp-controller="Authentication" asp-action="Register" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
<div class="form-group mb-3">
<label asp-for="Username" class="control-label"></label>
<input asp-for="Username" class="form-control" />
<span asp-validation-for="Username" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Email" class="control-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Password" class="control-label"></label>
<input asp-for="Password" class="form-control" type="password" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="ConfirmPassword" class="control-label"></label>
<input asp-for="ConfirmPassword" class="form-control" type="password" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<div class="form-group text-center mb-3">
<button type="submit" class="btn btn-primary">Register</button>
</div>
</form>
Displaying the user's claims
[Authorize]
[HttpGet("claims")]
public IActionResult Claims()
{
return View(model: new ClaimsViewModel
{
Claims = User.Claims.ToList()
});
}
// Authentication/Claims.cshtml
@model ClaimsViewModel
@if (Model.Claims != null)
{
<table class="table">
<thead>
<tr>
<th>Claim Type</th>
<th>Claim Value</th>
</tr>
</thead>
<tbody>
@foreach (var claim in Model.Claims)
{
<tr>
<td>@claim.Type</td>
<td>@claim.Value</td>
</tr>
}
</tbody>
</table>
}
else
{
<p>No claims found.</p>
}
Logout
[HttpGet("logout")]
public async Task<IActionResult> Logout()
{
_signInManager.AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme;
await _signInManager.SignOutAsync();
return RedirectToAction("Index", "Home");
}
Now, it is time to see the permissions in action. For this purpose, we define The AuthorizationController
providing role management functionalities.
public AuthorizationController(
RoleManager<ApplicationRole> roleManager,
IPermissionService permissionService)
{
_roleManager = roleManager;
_permissionService = permissionService;
}
RoleList
- Used for reviewing the list of all roles
-
[Authorize(Permissions.RoleView)]
: Requires the user to have the role:view permission to access this action.
RoleCreate
- Used for creating a new role
-
[Authorize(Policy = Permissions.RoleCreate)]
: Requires the user to have theRoleCreate
permission to access this action.
[Authorize(Permissions.RoleView)]
[HttpGet("roles")]
public async Task<IActionResult> RoleList()
{
var roles = await _roleManager.Roles.ToListAsync();
return View(new RolesViewModel { Roles = roles });
}
[Authorize(Policy = Permissions.RoleCreate)]
[HttpGet("roles/create")]
public IActionResult RoleCreate()
{
return View("RoleCreate", new CreateRoleViewModel { });
}
[Authorize(Policy = Permissions.RoleCreate)]
[HttpPost("roles")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RoleCreate(CreateRoleViewModel request)
{
if (!ModelState.IsValid)
{
return View("RoleCreate");
}
var role = new ApplicationRole
{
Name = request.Name
};
var result = await _roleManager.CreateAsync(role);
if (!result.Succeeded)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
return View("RoleCreate");
}
return RedirectToAction(nameof(RoleList));
}
// Authorization/RoleList.cshtml
@model RolesViewModel
@{
ViewData["Title"] = "Roles";
}
<h1>@ViewData["Title"]</h1>
<ul>
@foreach (var role in Model.Roles)
{
<li>@role</li>
}
</ul>
@if ((await AuthorizationService.AuthorizeAsync(User, Permissions.RoleCreate)).Succeeded)
{
<a asp-controller="Authorization" asp-action="RoleCreate">Create Role</a>
}
// Authorization/RoleCreate.cshtml
@model CreateRoleViewModel
@{
ViewData["Title"] = "Create Role";
}
<h1>@ViewData["Title"]</h1>
<form asp-controller="Authorization" asp-action="Roles" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
<div class="row">
<div class="col-md-6">
<div class="form-group mb-3">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
</div>
</div>
<div class="form-group mb-3">
<button type="submit" class="btn btn-primary">Add Role</button>
</div>
</form>
Inside layout file, we add new routes based on authentication state and authorization policies
// Shared/_Layout.cshtml
@if (User.Identity is not null && User.Identity.IsAuthenticated)
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Authentication" asp-action="Claims">Claims</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Authentication" asp-action="Logout">Logout</a>
</li>
@if ((await AuthorizationService.AuthorizeAsync(User, Permissions.RoleView)).Succeeded)
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Authorization" asp-action="Roles">Roles</a>
</li>
}
}
else {
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Authentication" asp-action="Register">Register</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Authentication" asp-action="Login">Login</a>
</li>
}
Click to see Project Source Code
Conclusion
By leveraging ASP.NET Core Identity and customizing the authorization process, developers can create a fine-grained permission system that controls access to various parts of the application.
Defining identity models is the foundational step where user roles and permissions are clearly outlined. With these models in place, the IPermissionService
can be utilized to retrieve permissions for users, ensuring that only authorized individuals can perform specific actions.
The PermissionAuthorizationRequirement
and PermissionAuthorizationHandler
classes work in tandem to enforce these permissions at runtime, while the PermissionClaimsTransformation
class dynamically adds permission claims to the user's identity during the authentication process. This ensures that permissions are always up-to-date and accurately reflect the user's current roles.
Furthermore, by creating a custom ApplicationAuthorizationPolicyProvider
, policies can be dynamically generated based on the permissions defined in the system. This approach simplifies the management of policies and ensures consistency across the application.
Controllers like AuthenticationController
and AuthorizationController
demonstrate how to implement these concepts in a real-world application. The AuthenticationController
handles user login and registration, ensuring that users are authenticated correctly and assigned appropriate roles. The AuthorizationController
manages role-based access to various parts of the application, demonstrating how to secure endpoints with custom policies.
Top comments (0)