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
Step 3: Modify the appsettings.json file by adding a Sql Server Database Connection String in it so that we can create a database named SecueiryDBArt where we will generate Identity tables using EF Core Code-First approach. The connection string is shown in listing 1:

 "ConnectionStrings": {
   "SecurityConnStr": "Data Source=.;Initial Catalog=SecurityDBArt;Integrated Security=SSPI;TrustServerCertificate=True"
 }
Listing 1: The Database Connection String    

Step 4: In the project, add a new ew folder named Models. In this folder, we will add a class file named SecueiryDbContext.cs. This file will have an implementation for SecurityDbContext class that we will used to generate database and identity tables in it. The code o this class is shown in Listing 2

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace Core7_RBS_minimalAPI.Models
{
    public class SecurityDbContext : IdentityDbContext
    {
        public SecurityDbContext(DbContextOptions<SecurityDbContext> options):base(options)
        {
        }
    }
}


Listing 2: The SecurityDbContext class
 
As shown in Listing 2, the SecurityDbContext class is derived from the IdentityDbContect base class. This base class uses IdentityUser, IdentityRole, etc. classes to create Users and Roles respectively in database that is generated using EF Code First Approach.    

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

Listing 4: Commands for EF Code First      

Step 6: Once the database is generated, we will work on creating the classes those will be used to create user, authenticate user, and create role, as well as to assign user to role. Listing 5 shows classes we will be used for this purpose. In the Models folder, add a new class file and name it as SecurityClasses.cs in this file we will add classes as shown in Listing 6


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;}
    }

}

Listing 5: Security classes

As shown in Listing 5, we have various classed for User and Role management. The important class is SecureResponse, this class we will be using to return response of operations which we will be implementing in creating users and roles. To create new Users, Roles and assign Roles to User and and for Authentication, we need UserManager, RoleManager, and SignInManager classes respectively. We need to register these classes in DI Container using AddIdentity () method. The code in Listing 6, shows the AddIdentity()  uses. Make sure that you write the code after the code shown in Listing 3

builder.Services.AddIdentity<IdentityUser, IdentityRole>()
    // Use the EF Core for Creating, Managing Users and Roles
    .AddEntityFrameworkStores<SecurityDbContext>();
Listing 6: Registering the Identity classes in DI Container 

Step 7: In the project, add a new folder and name it as Services. In this folder we will add class file named SecurityServices.cs. In this class file we will add code for creating new user, authenticating user, creating new role and assigning role to user as shown in Listing 7


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;
            
        }
 
    }
}

Listing 7: Security class

The SecurityService class is constructor injected using UserManager, RoleManager, and SignInManager objects. We have already registered these classes in DI container using AddIdentity() method as shown in Listing 6. The SecurityClass is used these classes for performing User and Role Management. As shown in Listing 7, each method is self-explanatory.

Step 8: Lets register the security service class in DI Container after AddIdentity() method class. The Listing 8 shows the registration of SecuityService class in DI Container in Program.cs


.....
builder.Services.AddScoped<SecurityServices>();
.....

Listing 8: The SecurityServie class registration

Now lets add services for Authentication and Authorization in DI Container. In ASP.NET Core we have Policies for Authorization, we will use these policies to authorize the API access. The code in Listing 9 shows the service registration in Program.cs. Add this code below the code shown in Listing 8


......
builder.Services.AddAuthentication();
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("read", policy => policy.RequireRole("Manager", "Clerk"));
......    

Listing 9: The Authentication and Authorization services registration with policies

The code in Listing 9, set the read policy for Manager and Clerk roles, this means that all users thsose are assigned to these tow roles will be ale to access the endpoint where this policy is applied.
To use the Authentication and Authorization lets add Middlewares in Program.cs. Add these middlewares after the comment "Configure the HTTP request Pipeline" as shown in Listing 10

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();

Listing 10: The Security Middlewares   


Step 9: Modify the Program.cs by adding Endpoints for register user, login user, create role, assign role for Creating New User, Login User, Creating Role, Assign Role as shown in Listing 11
// 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



Listing 11: The API Endpoints for User and Role Management

Carefully read the code in Listing 11, we have injected the SecurityService in each Endpoint so that when request s received on these endpoints methods from the SecurityService class will be invoked. We already have weatherforecast endpoint in the project so lets we will modify this endpoint to use the Authorization as shown in Listing 12


app.MapGet("/weatherforecast", () =>
{
  ...........
})
.WithName("GetWeatherForecast")
.WithOpenApi()
.RequireAuthorization("read"); // The Authorization with Policies
Listing 12: Modification of the weatherforecast endpoint

We have applied the "read" authorization policy is applied on this endpoint means all roles and hence users of these roles will be able to access this endpoint with HTTP request.

Run the application and test it using the Postman or Advanced REST Client (ARC)

The swagger test UI page will be shown in browser as shown in Figure 1




Figure 1: The Swagger Test Page

Test the Register User Endpoint as shown in Figure 2




Figure 2: The Register User Endpoint

Likewise, create users like user1@myapp.com, user2@myapp.com, user3@myapp.com

Now let's create Manager, Operator, and Clerk Roles as shown in Figure 3.



Figure 3: Creating Roles


Now Assign Manager Role to User1 and Clerk Role to User 2 as shown in Figure 4



Figure 4: Assign Role to User

Authenticate User by using the user1@myapp.com user as shown in Figure 5



Figure 5: Authenticating the User   

Now access the weatherforecast endpoint, you will be able to access it as shown in Figure 6



Figure 6: Accessing the Weatherforecast endpoint

Now lets assign Operator role to User3@myapp.com and login using this user.  Now try to access the weatherforecast endpoint the error will be generated as shown in Figure 7



Figure 7: Accessing the weatherforecast using user3@myapp.com 

Since the user3@myapp.com is of Operator role which is not in read policy the user3 cannot access the weatherfrecast endpoint.

That's it.

The code for this article can be downloaded from this link

Conclusion: The Minimal API security approach in syntax is different from the traditional Controller based API. 

Popular posts from this blog

Uploading Excel File to ASP.NET Core 6 application to save data from Excel to SQL Server Database

ASP.NET Core 6: Downloading Files from the Server

ASP.NET Core 6: Using Entity Framework Core with Oracle Database with Code-First Approach