ASP.NET Core: Implementing Role Based Security with Minimal API
ASP.NET Core Technology Stack is designed to provide superb experience for building modern web apps using Razor Pages, MVC, API, and Blazor. The ASP.NET Core API provides the best feature of building new generation data communication services and this can e further extended to design and develop Microservices based applications. While working with API we create Controllers and then we write HTTP Action methods with logic in it. This approach is traditional as well as increase the coding efforts (sometimes its is useful). But what if that we want to avoid this additional controller creation and other configuration with it? The minimal APIs are specially written for exactly same requirements where we can avoid unnecessary controllers which we generate using the scaffolding. Minimal APIs are a simplified approach for building fast HTTP APIs using ASP.NET Core. We can build fully functioning REST endpoints with minimal code and configuration.
But now we have a challenge here is that how can we secure these APIs, as we know that the traditional APIs can be secured using the Authorize attribute and we can use ASP.ET Core Identity Classes to implement user based and role based security. If you want to know the implementation of Role Based Security in ASP.NET Core API then read my complete article by clicking on the link provided below:
https://www.webnethelper.com/2022/03/aspnet-core-6-using-role-based-security.html
In this article we will work on implementing Role Based Security in ASP.NET Crore Minimal APIs.
Step 1: Open Visual Studio 2022 and create a new ASP.NET Core Minimal API project. Name this project as Core7_RBS_minimalAPI. This will create an API project with a default logic of Weatherforecast endpoint. We will use the same endpoint.
Step 2: We will be using ASP.NET Core Identity classes to create users and roles. We will store these users and roles in SQL Server database. We need to install Microsoft EntityFramework Core (EF Core) packages for this project. Install following packages:
- Microsoft.EntityFrameorkCore
- Microsoft.EntityFrameworkCore.Relational
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Design
- Microsoft.EntityFrameworkCore.Tools
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace Core7_RBS_minimalAPI.Models { public class SecurityDbContext : IdentityDbContext { public SecurityDbContext(DbContextOptions<SecurityDbContext> options):base(options) { } } }
Let's register this class in DI Container so that further we can use EF Core Code-First approach to generate database and tables in SQL Server. Open the Program.cs and register the SecurityDbContext class after the builder object created as shown in Listing 3
..... var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddDbContext<SecurityDbContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("SecurityConnStr")); }); ......
Listing 3: Registering the SecurityDbContext class in DI Container
With the code in Listing 3, we are registering the SecurityDbContext class in DI Container and we are also setting the Connection String with SQL Server database so that the database will be created in SQL Server.
Step 5: Open the command prompt (or Terminal) and navigate to the project folder and run the command as shown in the Listing 4 to generate SecurityDBArt database with ASP.NET Identity Tables in it
Generate Migrations
dotnet ef migrations add firstMigration -c Core7_RBS_minimalAPI.Models.SecurityDbContext
Update Database
dotnet ef database update -c Core7_RBS_minimalAPI.Models.SecurityDbContext
namespace Core7_RBS_minimalAPI.Models { public class RegisterUser { /// <summary> /// UNique EMail /// </summary> public string? Email { get; set; } public string? Password { get; set; } public string? ConfirmPassword { get; set; } } public class LoginUser { public string? Email { get; set; } public string? Password { get; set; } } public class SecureResponse { public string? UserName { get; set; } public string? Message { get; set; } public int StatucCode { get; set; } public string? Token { get; set; } } /// <summary> /// Class to create a new Role /// </summary> public class RoleData { public string? RoleName { get; set;} } /// <summary> /// Class to assign Role to User /// </summary> public class UserRole { public string? UserName { get; set;} public string? RoleName { get; set;} } }
builder.Services.AddIdentity<IdentityUser, IdentityRole>() // Use the EF Core for Creating, Managing Users and Roles .AddEntityFrameworkStores<SecurityDbContext>();
using Core7_RBS_minimalAPI.Models; using Microsoft.AspNetCore.Identity; namespace Core7_RBS_minimalAPI.Services { public class SecurityServices { /// <summary> /// For Creating and Managing Users /// </summary> UserManager<IdentityUser> _userManager; /// <summary> /// Managing USer Logins /// </summary> SignInManager<IdentityUser> _signInManager; /// <summary> /// Create an Manage Roles /// </summary> RoleManager<IdentityRole> _roleManager; /// <summary> /// USed to read onfiguration from the appsettings.json /// </summary> IConfiguration _config; /// <summary> /// Inject THe UserManager and SignInManager in DI Container /// These dependencies will be resolved using /// The 'AddIdentityService<IdentityUser, IdentityRole>(); /// </summary> /// <param name="userManager"></param> /// <param name="signInManager"></param> public SecurityServices(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, RoleManager<IdentityRole> roleManager, IConfiguration config) { _userManager = userManager; _signInManager = signInManager; _roleManager = roleManager; _config = config; } public async Task<SecureResponse> RegisterUserAsync(RegisterUser user) { SecureResponse response = new SecureResponse(); if (user == null) { response.StatucCode = 500; response.Message = "User Details are not passed"; } else { // CHeck if USer Already Exists var identityUser = await _userManager.FindByEmailAsync(user.Email); if (identityUser != null) { response.StatucCode = 500; response.Message = $"User {user.Email} is already exist"; } else { // Create user var newUser = new IdentityUser() { Email = user.Email, UserName = user.Email }; // Create a user by hashing password var result = await _userManager.CreateAsync(newUser,user.Password); if (result.Succeeded) { response.StatucCode = 201; response.Message = $"User {user.Email} is created successfully"; } else { response.StatucCode = 500; response.Message = $"Some error occurred while creating the user"; } } } return response; } public async Task<SecureResponse> AuthUser(LoginUser user) { SecureResponse response = new SecureResponse(); if (user == null) { response.StatucCode = 500; response.Message = "User login Details are not passed"; } else { // Check if User Already does not Exists var identityUser = await _userManager.FindByEmailAsync(user.Email); if (identityUser == null) { response.StatucCode = 500; response.Message = $"User {user.Email} is not present"; } else { // Autenticate the user // Get the LIst of ROles assigned to user var roles = await _userManager.GetRolesAsync(identityUser); if (roles.Count == 0) { response.StatucCode = 500; response.Message = $"The user {user.Email} does not belong to any role, ad hence the user cannot access the application"; } else { // Paramater 1: Login EMail // Parameter 2: PAssword // Parameter 3: Creaing Persistent Cookie on Browser, set it to false for API // Parameter 4: Invalid login attempts will lock the user from login (5 attempts default) var authStatus = await _signInManager.PasswordSignInAsync(user.Email, user.Password, false, lockoutOnFailure: true); if (authStatus.Succeeded) { response.StatucCode = 200; response.Message = $"User {user.Email} Logged in successfuly"; } else { response.StatucCode = 500; response.Message = $"Error Occurred for User {user.Email} Login"; } } } } return response; } public async Task<SecureResponse> CreateRoleAsync(RoleData role) { SecureResponse response = new SecureResponse(); if (role == null) { response.StatucCode = 500; response.Message = "Not a valid data"; } else { // Check is role exist var roleInfo = await _roleManager.FindByNameAsync(role.RoleName); if (roleInfo != null) { response.StatucCode = 500; response.Message = $"Role {role.RoleName} is already exist"; } else { var identityRole = new IdentityRole() { Name = role.RoleName, NormalizedName = role.RoleName}; // Create a role var result = await _roleManager.CreateAsync(identityRole); if (result.Succeeded) { response.StatucCode = 200; response.Message = $"Role {role.RoleName} is created successfully"; } else { response.StatucCode = 500; response.Message = "Error Occurred while creating role"; } } } return response; } public async Task<SecureResponse> AddRoleToUserAsync(UserRole userRole) { SecureResponse response = new SecureResponse(); if (userRole == null) { response.StatucCode = 500; response.Message = "No valid information is available"; } else { // 1. Check for role var role = await _roleManager.FindByNameAsync(userRole.RoleName); // 2. Check for user var user = await _userManager.FindByEmailAsync(userRole.UserName); if (role == null || user == null) { response.StatucCode = 500; response.Message = $"Either Role {userRole.RoleName} or User {userRole.UserName} is not available"; } else { // assing role to user var result = await _userManager.AddToRoleAsync(user, role.Name); if (result.Succeeded) { response.StatucCode = 200; response.Message = $"The User : {user.Email} is assgned to Role: {role.Name}"; } else { response.StatucCode = 500; response.Message = "Some error occurred while processing the user assignment to role request."; } } } return response; } } }
..... builder.Services.AddScoped<SecurityServices>(); .....
...... builder.Services.AddAuthentication(); builder.Services.AddAuthorizationBuilder() .AddPolicy("read", policy => policy.RequireRole("Manager", "Clerk")); ......
var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); // Middlewares for Authentication and Authorization app.UseAuthentication(); app.UseAuthorization();
// API Endpoints for User and Role Management app.MapPost("/registeruser", async (RegisterUser user, SecurityServices serv) => { var response = await serv.RegisterUserAsync(user); return Results.Ok(response); }); app.MapPost("/loginuser", async (LoginUser user, SecurityServices serv) => { var response = await serv.AuthUser(user); return Results.Ok(response); }); app.MapPost("/createrole", async (RoleData role, SecurityServices serv) => { var response = await serv.CreateRoleAsync(role); return Results.Ok(response); }); app.MapPost("/assigrole", async (UserRole userrole, SecurityServices serv) => { var response = await serv.AddRoleToUserAsync(userrole); return Results.Ok(response); }); // API Endpoints for User and Role Management Ends here
app.MapGet("/weatherforecast", () => { ........... }) .WithName("GetWeatherForecast") .WithOpenApi() .RequireAuthorization("read"); // The Authorization with Policies