ASP.NET Core 6: Using Role Based Security for ASP.NET Core 6 WEB API

In this article, we will see the complete implementation of the Role-Based Security for ASP.NET Core 6 WEB API. While building the FullStack application, we need to make sure that the Front-End application e.g. Angular, React.js. VueJS, Blazor, should access the WEB API securely by using a well-defined Authentication and Authorization mechanism. I have already published articles on ASP.NET Core WEB API Token-Based authentication on the following links

ASP.NET Core 5 and Blazor Web Assembly Apps: Token Based Authentication in ASP.NET Core 5 APIs and Authenticating it using Blazor WebAssembly Apps (webnethelper.com)

Understanding Token Based Authentication in ASP.NET Core 3.1 using JSON WEB TOKENS (webnethelper.com)

Authenticating Angular 8 Client Application with JSON WEB Token (JWT) Based Authentication (webnethelper.com)

In ASP.NET Core, the Policy-Based Authentication allows to club roles into a group and we can set the access policies of the API methods to these groups. We can provide access to specific action methods of API to these policies so that when the user is authenticated, the role of the user is extracted, and then based on the policy set for the role, the user is provided access to the API action method.   

In the example which we will be discussing in this article, the Order Management API. The following points are discussed in the code.

  1. The application has  Administrator, Manager, and Clerk roles. 
  2. The application roles can be created only by the Administrator. 
  3. The user can register himself, but the user can access the API only when the role is assigned to the user.
  4. The user can be assigned a role by the Administrator
  5. Orders can be created and accessed by Administrator, Manager, and Clerk Role.


Step 1: Open Visual Studio 2022 and create a new ASP.NET Core WEB API Project. Name this project as RbsAPI. Select the version as .NET 6 LTS. In this project add the following packages to use EF Core 

  1. Microsoft.EntityFrameworkCore
  2. Microsoft.EntityFrameworkCore.SqlServer
  3. Microsoft.EntityFrameworkCore.Relational
  4. Microsoft.EntityFrameworkCore.Design
  5. Microsoft.EntityFrameworkCore.Tools
We will use EF Core to access the database where we will store Application Users, Roles, and Orders.

Step 2: We will be using Global using so that individual code files will not import namespaces repeatedly. In the project add a new class file and name it as Using.cs. In this file add code as shown in listing 1


global using Microsoft.EntityFrameworkCore;
global using Microsoft.AspNetCore.Identity;
global using RbsAPI.Models;
global using RbsAPI.Repositories;
global using RbsAPI.AppBuilder;
global using Microsoft.IdentityModel.Tokens;
global using System.IdentityModel.Tokens.Jwt;
global using System.Security.Claims;
global using Microsoft.AspNetCore.Authorization;
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.AspNetCore.Authentication.JwtBearer;
global using Microsoft.OpenApi.Models;

Listing 1: The Global Using    

Listing 1 shows namespaces for Security, Token, EntityFrameworkCore, etc. Please note that namespaces like Models, AppBuilder, and Repositories are added to the project by adding folders with respective names in the next steps.

Step 2:  In the project add a new folder and name it as Models. In this folder add a new class file and name it as Orders.cs. In this class file, we will add code for the Orders class so that Orders information can be accessed from the end-user. The code for the Orders class is shown in listing 2


using System.ComponentModel.DataAnnotations;

namespace RbsAPI.Models
{
    public class Orders
    {
        [Key]
        public int OrderUniqueId { get; set; }
        [Required(ErrorMessage = "Order Id is Required")]
        public string OrderId { get; set; }
        [Required(ErrorMessage = "ItemName is Required")]
        public string ItemName { get; set; }
        [Required(ErrorMessage = "CustomerName is Required")]
        public string CustomerName { get; set; }
        [Required(ErrorMessage = "Quantity is Required")]
        public int Quantity { get; set; }
        [Required(ErrorMessage = "UnitPrice is Required")]
        public int UnitPrice { get; set; }
        [Required(ErrorMessage = "TotalPrice is Required")]
        public int TotalPrice { get; set; }
        [Required(ErrorMessage = "Created By is Required")]
        public string CreatedBy { get; set; }
        public DateTime CreatedDate { get; set; }
        public string UpdatedBy { get; set; }
        public DateTime UpdatedDate { get; set; }
        public bool IsOrderApproved { get; set; }
    }
}

Listing 2: The Orders class

The Orders class contains properties for storing the Orders information in a database table.  In the Models folder, add a new class file and name it as OrdersDbContext.cs. In this file add the code as shown in listing 3


namespace RbsAPI.Models
{
    public class OrdersDbContext : DbContext
    {
        public DbSet<Orders> Orders { get; set; }
        public OrdersDbContext(DbContextOptions<OrdersDbContext> options) : base(options)
        {
        }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);
        }
    }
}

Listing 3: The OrdersDbContext class 

The OrdersDbContext class is derived from DbContext class so that it can create a database and map with the Orders table so that Orders information is saved in the database.

Step 3:  Now we need to add classes for Users, Roles, UerInRoles, etc. those will be used by the application for the security implementation. In the Models folder, add a new class file and name it as UserModels.cs. In this class, file add the code as shown in listing 4


using System.ComponentModel.DataAnnotations;

namespace RbsAPI.Models
{
    /// <summary>
    /// Class for Login Information
    /// </summary>
    public class LoginUser
    {
        [Required(ErrorMessage = "User Name is Required")]
        public string UserName { get; set; }

        [Required(ErrorMessage = "Password is Required")]
        public string Password { get; set; }
         
    }

    /// <summary>
    /// Class for Registering User
    /// </summary>
    public class RegisterUser
    {
        [Required(ErrorMessage = "Email is Required")]
        [EmailAddress]
        public string Email { get; set; }
        [Required(ErrorMessage = "Password is Required")]
        [RegularExpression("^((?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])|(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[^a-zA-Z0-9])|(?=.*?[A-Z])(?=.*?[0-9])(?=.*?[^a-zA-Z0-9])|(?=.*?[a-z])(?=.*?[0-9])(?=.*?[^a-zA-Z0-9])).{8,}$",
            ErrorMessage = "Passwords must be minimum 8 characters and can contain upper case, lower case, number (0-9) and special character")]
        public string Password { get; set; }
        [Compare("Password")]
        public string ConfirmPassword { get; set; }
    }

    public class ApplicationRole
    {
        public string Name { get; set; }
        public string NormalizedName { get; set; }
    }

    /// <summary>
    /// Class for Approving User to Assign Role to it
    /// </summary>
    public class UserRole
    {
        public string UserName { get; set; }
        public string RoleName { get; set; }
    }

    public class Users
    {
        public string Email { get; set; }
        public string UserName { get; set; }
    }
}

Listing 4: User Models    

Classes in listing 1 have the following uses

  1. LoginUser: This class is used to access Login Information from the end-user
  2. Register: This class is used to register a new user to the application
  3. ApplicationRole: The class used to create a role for the application
  4. UserRole: The class to assign a role to the user
  5. Users: The class used to retrieve the user's information
In the Models folder, add a new class file and name it as RBSAuthDbContext.cs. In this class, file add the code as shown in listing 5

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

namespace RbsAPI.Models
{
    public class RBSAuthDbContext : IdentityDbContext<IdentityUser>
    {
        public RBSAuthDbContext(DbContextOptions<RBSAuthDbContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            // Customize the ASP.NET Identity model and override the defaults if needed.
            // For example, you can rename the ASP.NET Identity table names and more.
            // Add your customizations after calling base.OnModelCreating(builder);
        }
    }
}

Listing 5: The RBSAuthDbContext class  
The RBSAuthDbContext class is derived from the  IdentityDbContext<IdentityUser> class. This class is used to create ASP.NET Core Identity tables in the database.

Step 4: In the Models folder, add a new class file and name it as AuthResponseStatus.cs. In this class file, we will add classes for returning the Authentication status using ResponseStatus class and the state of logic using the LoginStaus enum. The code is shown in listing 6
 

namespace RbsAPI.Models
{
    /// 
    /// The class for response from the WEB API
    /// 
    public class AuthStatus
    {
        public LoginStatus LoginStatus { get; set; }
        public string Token { get; set; }
        public string Role { get; set; }
    }
    public enum LoginStatus 
    {
         NoRoleToUser = 0,
         LoginFailed = 1,
         LoginSuccessful = 2
    }
    public class ResponseStatus
    {
        public int StatusCode { get; set; }
        public string Message { get; set; }
        public string Token { get; set; }
        public string UserName { get; set; }
        public string Role { get; set; }
    }
}

Listing 6:  AuthStatus classes and Enum

Step 5: Let's modify the appsettinhgs.json by adding connection strings for the Application Database and Security database where we will have Orders and ASP.NET Core Identity tables respectively. Listing 7 shows the connection string

"ConnectionStrings": {
    "RBSAuthDbContextConnection": "Server=localhost;Database=RBSAuthDBNet6;Integrated Security=SSPI;MultipleActiveResultSets=true",
    "RBSAppDbContextConnection": "Server=localhost;Database=RBSAppDBNet6;Integrated Security=SSPI;MultipleActiveResultSets=true"
  }

Listing 7: The Connection String

Step 6: Open the Program.cs and register OrdersDbContext and RBSAuthDbContext classes in the Dependency Injection Container as shown in listing 8


builder.Services.AddDbContext<RBSAuthDbContext>(options =>
{
    options.UseSqlServer(builder.Configuration
        .GetConnectionString("RBSAuthDbContextConnection"));
});

builder.Services.AddDbContext<OrdersDbContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString
    ("RBSAppDbContextConnection"));
});

builder.Services.AddIdentity<IdentityUser, IdentityRole>()
        .AddEntityFrameworkStores<RBSAuthDbContext>()
    .AddDefaultTokenProviders();
Listing 8: The Dependency Container registration for OrdersDbContext and RBSAuthDbContext classes 
Listing 8 shows the registration of DbContext classes in DI Container and also the Identity Provider is also configured for User and Role class mapping with the database. The AddIdentity() method will register RoleManager and UserManaer classes in Dependency Injection Container.

Step 7: Open the Command Prompt and navigate to the Project Folder and run commands as shown in listing 9 to generate database Migrations and Update database

dotnet ef migrations add securityMigrations -c RbsAPI.Models.RBSAuthDbContext

dotnet ef database update -c RbsAPI.Models.RBSAuthDbContext


dotnet ef migrations add appMigrations -c RbsAPI.Models.OrdersDbContext

dotnet ef database update -c RbsAPI.Models.OrdersDbContext


Listing 9: Commands to generate database migrations and update database 

After running these commands, the database will be generated in SQL Server database of name RBSAppDbNet6 and RBSAuthDbNet6

Step 8: Once the database is created now its time for us to work with the logic for performing the CRUD operations on the Orders table. In the project, add a new folder and name it as Repositories.  In this folder add a new file and name it as OrdersService.cs. In this class file, add the code as shown in listing 10. 

namespace RbsAPI.Repositories
{
    public class OrdersService  
    {
        OrdersDbContext ctx;
        public OrdersService(OrdersDbContext ctx)
        {
            this.ctx = ctx;
        }

        public async Task<Orders> CreateAsync(Orders entity)
        {
             
            entity.TotalPrice = entity.UnitPrice * entity.Quantity;
            var res = await ctx.Orders.AddAsync(entity);
            await ctx.SaveChangesAsync();
            return res.Entity;
        }

        public async Task<bool> DeleteAsync(int id)
        {
            bool isDeleted = false;
            var order = await ctx.Orders.FindAsync(id);
            if (order != null)
            {
                ctx.Orders.Remove(order);
                await ctx.SaveChangesAsync();
                isDeleted = true;
            }
            return isDeleted;
        }

        public async Task<IEnumerable<Orders>> GetAsync()
        {
            return await ctx.Orders.ToListAsync();
        }

        public async Task<IEnumerable<Orders>> GetNotApprovedOrdersAsync()
        {
            return await ctx.Orders.Where(ord=>ord.IsOrderApproved == false).ToListAsync();
        }


        public async Task<Orders> GetAsync(int id)
        {
           return  await ctx.Orders.FindAsync(id);
        }

        public async Task<bool> UpdateAsync(int id, Orders entity)
        {
            bool isUpdated = false;
            var order = await ctx.Orders.FindAsync(id);
            // update order if it is not already approved
            if (order != null || !order.IsOrderApproved)
            {
                order.CustomerName = entity.CustomerName;
                order.ItemName = entity.ItemName;
                order.Quantity = entity.Quantity;
                order.TotalPrice = entity.UnitPrice * entity.Quantity;
                order.UpdatedBy = entity.UpdatedBy;
                order.UpdatedDate = entity.UpdatedDate;
                await ctx.SaveChangesAsync();
                isUpdated = true;
            }
            return isUpdated;
        }

        public async Task<bool> ApproveOrderAsync(int id)
        {
            bool isUpdated = false;
            var order = await ctx.Orders.FindAsync(id);
            // update order if it is not already approved then only approve it
            if (order != null || !order.IsOrderApproved)
            {
                order.IsOrderApproved = true;
                await ctx.SaveChangesAsync();
                isUpdated = true;
            }
            return isUpdated;
        }
    }
}


Listing 10: The OrdersService class

The code in Listing 10 shows that the OrdersService class is injected with OrdersDbContext class. The OrdersService performs class performs the CRUD operations on the Orders table.

Step 9: Let's modify the appsettings.json file to add a new key for storing the SecretKey for generating the token and the token expiry time. The listing 11 shows the keys for Expity time and secret key


 "JWTCoreSettings": {
    "SecretKey": "f4LZOS1MJ+lwLI+NZDSatxQffwf4CMnCUyAJaEcd/tm5tcLhXkuV9bO+bYF+NgdmrJqE69LDDiQotz0rQIfJqw==",
    "ExpiryInMinuts": 20
  }
Listing 11: The Keys in the appsettings.json   

The Secret key is generated using the cryptography code. You can refer it from the listing 12
namespace Core_JWTKey_Generator
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var rNGCryptoServiceProvider = new RNGCryptoServiceProvider())
            {
                var SigningSecretKey = new byte[64];
                rNGCryptoServiceProvider.GetBytes(SigningSecretKey);
                Console.WriteLine($"Secret Key is {Convert.ToBase64String(SigningSecretKey)}");
            }
            Console.ReadLine();
        }
    }
}


Listing 12: The Key Generator

Step  10: In the Repositories folder, add a new class file and name it as SecurityService.cs. In this class file add the code as showein in listing 13
namespace RbsAPI.Repositories
{
    /// <summary>
    /// The following class contains methods for
    /// 1. Create a new Role
    /// 2. Create a new User and assign Role to User
    /// 3. Manage the User Login and generate token
    /// </summary>
    public class AuthSecurityService
    {
        IConfiguration configuration;
        SignInManager<IdentityUser> signInManager;
        UserManager<IdentityUser> userManager;
        RoleManager<IdentityRole> roleManager;
        public AuthSecurityService(IConfiguration configuration,
            SignInManager<IdentityUser> signInManager,
            UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager)
        {
            this.configuration = configuration;
            this.signInManager = signInManager;
            this.userManager = userManager;
            this.roleManager = roleManager;
        }

        public async Task<bool> CreateRoleAsync(IdentityRole role)
        {
            bool isRoleCreated = false;
            var res = await roleManager.CreateAsync(role);
            if (res.Succeeded)
            {
                isRoleCreated = true;
            }
            return isRoleCreated;
        }

        public async Task<List<ApplicationRole>> GetRolesAsync()
        {
            List<ApplicationRole> roles = new List<ApplicationRole>();
            roles = (from r in await roleManager.Roles.ToListAsync()
                     select new ApplicationRole()
                     { 
                         Name = r.Name,
                         NormalizedName = r.NormalizedName
                     }).ToList();
            return roles;
        }

        public async Task<List<Users>> GetUsersAsync()
        {
            List<Users> users = new List<Users>();
            users = (from u in await userManager.Users.ToListAsync()
                    select new Users()
                    {
                         Email = u.Email,
                         UserName = u.UserName
                    }).ToList();
            return users;
        }
        public async Task<bool> RegisterUserAsync(RegisterUser register)
        {
            bool IsCreated = false;
            
            var registerUser = new IdentityUser() { UserName = register.Email, Email = register.Email };
            var result = await userManager.CreateAsync(registerUser, register.Password);
            if (result.Succeeded)
            {
                IsCreated = true;
            }
            return IsCreated;
        }
        /// <summary>
        /// Method to Assign Role to User
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
        public async Task<bool> AssignRoleToUserAsync(UserRole user)
        {
            bool isRoleAssigned = false;
            // find role associated with the RoleName
            var role = roleManager.FindByNameAsync(user.RoleName).Result;
            // var registeredUser = new IdentityUser() { UserName = user.User.UserName};
            // find user by name
            var registeredUser = await userManager.FindByNameAsync(user.UserName);
            if (role != null)
            {
               var res =  await userManager.AddToRoleAsync(registeredUser, role.Name);
               if (res.Succeeded)
               {
                    isRoleAssigned = true;
               }
            }
            return isRoleAssigned;
        }

        /// <summary>
        /// Class to Authenticate User based on User Name
        /// </summary>
        /// <param name="inputModel"></param>
        /// <returns></returns>
        public async Task<AuthStatus> AuthUserAsync(LoginUser inputModel)
        {
            string jwtToken = "";
            LoginStatus loginStatus;
            string roleName = "";
            var result = signInManager.PasswordSignInAsync(inputModel.UserName, 
            inputModel.Password, false, lockoutOnFailure: true).Result;
            if (result.Succeeded)
            {

                // Read the secret key and the expiration from the configuration 
                var secretKey = Convert.FromBase64String(configuration["JWTCoreSettings:SecretKey"]);
                var expiryTimeSpan = Convert.ToInt32(configuration["JWTCoreSettings:ExpiryInMinuts"]);
                // logic to get the user role
                // get the user object based on Email
                // IdentityUser user = new IdentityUser(inputModel.UserName);
                var user = await userManager.FindByEmailAsync(inputModel.UserName);
                var role = await userManager.GetRolesAsync(user);
                // if user is not associated with role then log off
                if (role.Count == 0)
                {
                    await signInManager.SignOutAsync();
                    loginStatus =  LoginStatus.NoRoleToUser;
                }
                else
                {
                    //read the rolename
                    roleName = role[0];
                    // set the expity, subject, etc.
                    // note that Issuer and Audience will be null because 
                    // there is no third-party issuer
                    var securityTokenDescription = new SecurityTokenDescriptor()
                    {
                        Issuer = null,
                        Audience = null,
                        Subject = new ClaimsIdentity(new List<Claim> {
                        new Claim("userid",user.Id.ToString()),
                        new Claim("role",role[0])
                    }),
                        Expires = DateTime.UtcNow.AddMinutes(expiryTimeSpan),
                        IssuedAt = DateTime.UtcNow,
                        NotBefore = DateTime.UtcNow,
                        SigningCredentials = new SigningCredentials
                        (new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature)
                    };
                    // Now generate token using JwtSecurityTokenHandler
                    var jwtHandler = new JwtSecurityTokenHandler();
                    var jwToken = jwtHandler.CreateJwtSecurityToken(securityTokenDescription);
                    jwtToken = jwtHandler.WriteToken(jwToken);
                    loginStatus = LoginStatus.LoginSuccessful;
                }
            }
            else
            {
                loginStatus = LoginStatus.LoginFailed;
            }
            var authResponse = new AuthStatus()
            {
                 LoginStatus = loginStatus,
                 Token = jwtToken,
                 Role = roleName
            };
            return authResponse;
        }

        /// <summary>
        /// Thie method willaccept the token as inout parameter and wil receive token from it
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        public async Task<string> GetUserFromTokenAsync(string token)
        {
            string userName = "";
            var jwtHandler = new JwtSecurityTokenHandler();
            // read the token values
            var jwtSecurityToken = jwtHandler.ReadJwtToken(token);
            // read claims
            var claims = jwtSecurityToken.Claims;
            // read first claim
            var userIdClaim = claims.First();
            // read the user Id
            var userId = userIdClaim.Value;
            // get the username from the userid
            var identityUser = await userManager.FindByIdAsync(userId);
            userName = identityUser.UserName;
            return userName;
        }

        public string GetRoleFormToken(string token)
        {
            string roleName = "";
            var jwtHandler = new JwtSecurityTokenHandler();
            // read the token values
            var jwtSecurityToken = jwtHandler.ReadJwtToken(token);
            // read claims
            var claims = jwtSecurityToken.Claims;
            // read first two claim
            var roleClaim = claims.Take(2);
            // read the role
            var roleRecord = roleClaim.Last();
            // read the role name
            roleName = roleRecord.Value;
            return roleName;
        }
        
    }
}



Listing 13: The AuthSecurityService class

The AuthSecurityService class is the heart of the complete application. This class contains various methods. The behavior of each method is explained in  the following points
  • The Constructor: The Constructor is injected with various interfaces as follows
    • IConfiguration: This is used to read various keys that are defined in appsettings.json
    • SignInManager: This class is used to manage the User SignIn process
    • UserManager: This is used to Register new users with the application
    • RoleManager: This is used to create a new role
  • CreateRoleAsync: This method accepts the IdentityRole class as an input parameter. This method uses IdentityRole object to create a new Role for the application.
  • GetRolesAsync: This method is used to read the list of roles registered for the application
  • GetUsersAsync: This method is used to read the list of all users registered with the application
  • RegisterUserAsync: This method accepts a RegisterUser class object to register a new user with the application.
  • AssignRoleToUserAsync: This method accepts UserRole object as input parameter. This method checks if the role and user are already registered with the application. If they are present then the user will be assigned the role.
  • AuthUserAsync: This method accepts LoginUser object as an input parameter. This method Sign in the user with the application. Once the user is signed in with the application then the JSON Web Token will be generated by reading Security Key and Expiry time from the appsettings.json. The token is generated by adding UserId, Role Name in the claim. If the user is not assigned the role, then the user will not be able to log in with the application. Once the token is generated the login status is returned by the method. 
  • GetUserFromTokenAsync: This method accepts the token as an input parameter. The method reads the claim from the token and then reads the username from the token and returns it.
  • GetRoleFromToken: This method accepts the token as an input parameter. The method reads the claim from the token and then reads rolename from the token and returns it.        
Step 11: Modify the Program.cs and register OrdersService and AuthSecurityService classes in the Dependency Injection Container, Along with these we need to add CORS policy service and Role Policies for Authorization as shown in listing 14. 


builder.Services.AddCors(options =>
{
    options.AddPolicy("corspolicy", (policy) =>
    {
        policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
    });
});


// register all services (repositories)
builder.Services.AddScoped<AuthSecurityService>();
builder.Services.AddScoped<OrdersService>();


builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminPolicy", (policy) =>
    {
        policy.RequireRole("Administrator");
    });

    options.AddPolicy("AdminManagerPolicy", (policy) =>
    {
        policy.RequireRole("Administrator", "Manager");
    });

    options.AddPolicy("AdminManagerClerkPolicy", (policy) =>
    {
        policy.RequireRole("Administrator", "Manager", "Clerk");
    });
});
Listing 14: The Services Registration in Dependency Injection Container  

As shown in Listing 14,  we have defined policies for Administrators, Managers, and Clerks. We will be creating an Administrator Role when the application runs for the first time and then the Administrator can create other roles for the application. 

Step 12: Let's modify the Program.cs by adding code for validating the JSON token.  The code is shown in listing 15

// Read the Secret Key from the appsettings.json
byte[] secretKey = Convert.FromBase64String(builder.Configuration["JWTCoreSettings:SecretKey"]);
// set the Authentication Scheme
builder.Services.AddAuthentication(x =>
{
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
    // Validate the token bt receivig the token from the Authorization Request Header
    x.RequireHttpsMetadata = false;
    x.SaveToken = true;
    x.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(secretKey),
        ValidateIssuer = false,
        ValidateAudience = false
    };
    x.Events = new JwtBearerEvents()
    {
        // If the Token is expired the respond
        OnAuthenticationFailed = context =>
        {
            if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
            {
                context.Response.Headers.Add
                ("Authentication-Token-Expired", "true");
            }
            return Task.CompletedTask;
        }
    };
}).AddCookie(options =>
{
    options.Events.OnRedirectToAccessDenied =
    options.Events.OnRedirectToLogin = c =>
    {
        c.Response.StatusCode = StatusCodes.Status401Unauthorized;
        return Task.FromResult<object>(null);
    };
});


Listing 15: The token validation

The code is listing 15 plays a very important role by defining the Bearer Token validation for the Request Header. The AddJwtBearer() method will be used to extract the Token from the Authorization Request header and validate it. If the token is expired the JwtBearerEvents class will be used to write the Response Header.

Step 13: We will modify the Program.cs by adding JSON options for the Entity Property Serialization by setting Property Naming Policy as null as shown in lisitng 16
builder.Services.AddControllers().AddJsonOptions(options => {
    options.JsonSerializerOptions.PropertyNamingPolicy = null;
});

Listing 16: The Property Naming policy 

Since the ASP.NET Core 5 onwards, the Swagger Generator is by default integrated for testing API, we will modify the Swagger Generator to add JSON Web Token Authorization Configuration using AddSecurityDefinition() method of the SwaggerGenOptions class as shown in listing 17

builder.Services.AddSwaggerGen(options =>
{
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "JSON Web Token Authorization header using the Bearer scheme.
        \"Authorization: Bearer {token}\"",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.Http,
        Scheme = "Bearer"
    });


    options.AddSecurityRequirement(new OpenApiSecurityRequirement()
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                },
            },
            new List<string>()
        }
    });
});

Listing 17: Swagger Generation Options for Authorization Security Definition     
  
As shown in Listing 17, the Security definition is added for HTTP Requests and the Authorization scheme as Bearer. This will provide the Authorize button on the Swagger UI page. 

Step 14: We need to create a default Administrator role and add a new user to it. To do this let's add a new folder in the application and name it as AppBuilder. In this folder add a new class file and name it GlobalOPs.cs. In this class, file add the code as shown in listing 18

namespace RbsAPI.AppBuilder
{
    public static class GlobalOps
    {
        public static async Task CreateApplicationAdministrator(IServiceProvider serviceProvider)
        {
            try
            {
                // retrive instances of the RoleManager and UserManager 
                //from the Dependency Container
                var roleManager = serviceProvider
                	.GetRequiredService<RoleManager<IdentityRole>>();
                var userManager = serviceProvider
                	.GetRequiredService<UserManager<IdentityUser>>();

                IdentityResult result;
                // add a new Administrator role for the application
                var isRoleExist = await roleManager
                	.RoleExistsAsync("Administrator");
                if (!isRoleExist)
                {
                    // create Administrator Role and add it in Database
                    result = await roleManager
                    	.CreateAsync(new IdentityRole("Administrator"));
                }

                // code to create a default user and add it to Administrator Role
                var user = await userManager
                	.FindByEmailAsync("mahesh@myapp.com");
                if (user == null)
                {
                    var defaultUser = new IdentityUser() 
                    {
                    	UserName = "mahesh@myapp.com",
                        Email = "mahesh@myapp.com" 
                    };
                    var regUser = await userManager
                    	.CreateAsync(defaultUser, "P@ssw0rd_");
                    await userManager
                    	.AddToRoleAsync(defaultUser, "Administrator");
                }
            }
            catch (Exception ex)
            {
                var str = ex.Message;
            }

        }
    }
}


Listing 18: Code to create Administrator Role and User   

The code in listing 18, retrieves the UserManager and RoleManager objects from the Dependency Container. We have already registered these classes in the Dependency Container as shown code in listing 8. The code further creates a new Administrator role if it is not already available. Further, the new user of name mahesh@myapp.com is added and the Administrator role is assigned to it.

Step 15: Let's modify the Program.cs to use the CORS middleware and invoke the CreateApplicationAdministrator() method in it as shown in listing 19

app.UseCors("corspolicy");
.....
// Create DefaultAdminsistrator User
IServiceProvider serviceProvider = builder.Services.BuildServiceProvider();
await GlobalOps.CreateApplicationAdministrator(serviceProvider);

Listing 19: The modification in Program.cs
As shown in Listing 19,  we are retrieving IServiceProvider from the ServiceProvider collection, so that we can use the IServiceProvider to support the custom object for creating the default Administrator Role and user for the application.

Step 16: In the Controllers folder, add a new empty API controller and name it as SecurityController. In this controller, we will inject the AuthSecureityService using constructor injection so that we can access methods for creating a role, user, defined in AuthSecureityService class. The code of the SecurityController is shown in listing 20
namespace RbsAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class SecurityController : ControllerBase
    {
        private readonly AuthSecurityService service;
        public SecurityController(AuthSecurityService service)
        {
            this.service = service;
        }
        [Authorize(Policy = "AdminPolicy")]
        [Route("roles/readall")]
        [HttpGet]
        public async Task<IActionResult> GetRolesAsync()
        {
            ResponseStatus response;
            try
            {
                var roles = await service.GetRolesAsync();
                return Ok(roles);
            }
            catch (Exception ex)
            {
                response = SetResponse(400, ex.Message,"" ,"");
                return BadRequest(response);
            }
        }

        [Authorize(Policy = "AdminPolicy")]
        [Route("users/readall")]
        [HttpGet]
        public async Task<IActionResult> GetUsersAsync()
        {
            ResponseStatus response;
            try
            {
                var users = await service.GetUsersAsync();
                return Ok(users);
            }
            catch (Exception ex)
            {
                response = SetResponse(400, ex.Message,"", "");
                return BadRequest(response);
            }
        }
        [Authorize(Policy = "AdminPolicy")]
        [Route("post/role/create")]
        [HttpPost]
        public async Task<IActionResult> PostRoleAsync(ApplicationRole role)
        {
            ResponseStatus response;
            try
            {
                IdentityRole roleInfo = new IdentityRole()
                {
                     Name = role.Name,
                     NormalizedName = role.NormalizedName
                };
                var res  = await service.CreateRoleAsync(roleInfo);
                if (!res)
                {
                    response = SetResponse(500, "Role Registration Failed","", "");
                    return StatusCode(500, response);
                }
                response = SetResponse(200, $"{role.Name} is Created sussessfully","", "");
                return Ok(response);
            }
            catch (Exception ex)
            {
                response = SetResponse(400, ex.Message,"", "");
                return BadRequest(response);
            }
        }
        [Route("post/register/user")]
        [HttpPost]
        public async Task<IActionResult> RegisterUserAsync(RegisterUser user)
        {
            ResponseStatus response;
            try
            {
                var res = await service.RegisterUserAsync(user);
                if (!res)
                {
                    response = SetResponse(500, "User Registration Failed","","");
                    return StatusCode(500, response);
                }
                response = SetResponse(200, $"User {user.Email} is 
                        Created sussessfully","","");
                return Ok(response);
            }
            catch (Exception ex)
            {
                response = SetResponse(400, ex.Message,"","");
                return BadRequest(response);
            }
        }

        [Authorize(Policy = "AdminPolicy")]
        [Route("post/activate/user")]
        [HttpPost]
        public async Task<IActionResult> ActivateUserAsync(UserRole user)
        {
            ResponseStatus response;
            try
            {
                var res = await service.AssignRoleToUserAsync(user);
                if (!res)
                {
                    response = SetResponse(500, "Role is not assigned to user","","");
                    return StatusCode(500, response);
                }
                response = SetResponse(200, "Role is sussessfully assigned to user","","");
                return Ok(response);
            }
            catch (Exception ex)
            {
                response = SetResponse(400, ex.Message,"","");
                return BadRequest(response);
            }
        }

        [Route("post/auth/user")]
        [HttpPost]
        public async Task<IActionResult> AuthUserAsync(LoginUser user)
        {
            ResponseStatus response = new ResponseStatus();
            try
            {
                var res = await service.AuthUserAsync(user);
                if (res.LoginStatus == LoginStatus.LoginFailed)
                {
                    response = SetResponse(401,"UserName or Password is not found", "","");
                    return Unauthorized(response);
                }
                if (res.LoginStatus == LoginStatus.NoRoleToUser)
                {
                    response =  SetResponse(401, "User is not activated with role. 
                                 Please contact admin on mahesh@myapp.com","","");
                    return Unauthorized(response);
                }
                if (res.LoginStatus == LoginStatus.LoginSuccessful)
                {
                    response = SetResponse(200, "Login Sussessful", res.Token, res.Role);
                    response.UserName = user.UserName;
                    return Ok(response);
                }
                else
                {
                    response = SetResponse(500, "Internal Server Error Occured","","");
                    return StatusCode(500,response);
                }
            }
            catch (Exception ex)
            {
                response = SetResponse(400, ex.Message,"","");
                return BadRequest(response);
            }
        }

        /// <summary>
        /// Method to Set the Response
        /// </summary>
        /// <param name="code"></param>
        /// <param name="message"></param>
        /// <returns></returns>
        private ResponseStatus SetResponse(int code, string message, string token, .
                             string role)
        {
            ResponseStatus response = new ResponseStatus()
            { 
               StatusCode = code,
               Message = message,
               Token = token,
               Role = role
            };
            return response;
        }
    }
}

Listing 20: The SecurityController

Carefully read the above code, GetRolesAsync(), GetUsersAsync(), PostRoleAsync(), and ActivateUserAsync() methods are authorized to AdminPolicy. We already have defined AdminPolicy to Administrator role, we have done this in listing 14.   This means that these methods are accessible by users with Administrator roles only.   All methods in the SecurityController invoke methods from the AuthSecurityServive class.

Step 17: In the Controllers folder, add a new empty API controller and name it as OrdersController. In this controller, the AuthSecurityService and OrdersService are injected using constructor inject in the OrdersController class. In this controller add the code as shown in the listing 21

namespace RbsAPI.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    public class OrdersController : ControllerBase
    {
        private readonly OrdersService repository;
        private readonly AuthSecurityService service;

        public OrdersController(OrdersService repository, AuthSecurityService service)
        {
            this.repository = repository;
            this.service = service;
        }

        [HttpGet] 
        [ActionName("orders")]
        [Authorize(Policy = "AdminManagerClerkPolicy")]
        public async Task<IActionResult> Get()
        {
            try
            {
                GetRequestInfo(Request, out string userName, out string roleName);
                var orders = await repository.GetAsync();
                if (roleName == "Administrator")
                {
                    return Ok(orders);
                }
                var ordersByUserName = orders.Where(ord => ord.CreatedBy == userName.Trim());
                return Ok(ordersByUserName);
            }
            catch (Exception ex)
            {
                Response.Headers.Add("Error", ex.Message);
                return NoContent();
            }
        }
        [HttpGet("{id}")]
        [ActionName("orders")]
        [Authorize(Policy = "AdminManagerClerkPolicy")]
        public async  Task<IActionResult> Get(int id)
        {
            try
            {
                Orders order = await repository.GetAsync(id);
                if (order == null)
                {
                    return NotFound($"Record based on {id} is not found.");
                }
                return Ok(order);
            }
            catch (Exception ex)
            {
                Response.Headers.Add("Error", ex.Message);
                return NoContent();
            }
        }

        // Orders can be created by Administrator, Manager and Clerk

        [HttpPost]
        [ActionName("saveorder")]
        [Authorize(Policy = "AdminManagerClerkPolicy")]
        public async Task<IActionResult> Post(Orders orders)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    GetRequestInfo(Request, out string userName, out string roleName);
                    orders.CreatedDate = DateTime.Today;
                    orders.CreatedBy = userName;
                    orders.UpdatedBy = userName;
                    orders = await repository.CreateAsync(orders);
                    return Ok(orders);
                }
                else
                {
                    return BadRequest(ModelState);
                }
            }
            catch (Exception ex)
            {
                Response.Headers.Add("Error", ex.Message);
                return NoContent();
            }
        }

        // Non- Approved Orders can be updated by Administrator, Manager and Clerk
        [HttpPut("{id}")]
        [ActionName("updateorder")]
        [Authorize(Policy = "AdminManagerClerkPolicy")]
        public async Task<IActionResult> Put(int id, Orders orders)
        {
            try
            {
                if (id != orders.OrderUniqueId)
                {
                    return Conflict($"The headers {id} does not match with {orders.OrderUniqueId}");
                }
                if (ModelState.IsValid)
                {
                    GetRequestInfo(Request, out string userName, out string roleName);
                    orders.UpdatedDate = DateTime.Today;
                    orders.UpdatedBy = userName;
                    var response = await repository.UpdateAsync(id,orders);
                    if(response) return Ok(orders);
                    return NotFound($"Record based on {id} is not found.");
                }
                else
                {
                    return BadRequest(ModelState);
                }
            }
            catch (Exception ex)
            {
                Response.Headers.Add("Error", ex.Message);
                return NoContent();
            }
        }


        // Non- Approved Orders can be updated by Administrator, Manager  
        [HttpPut("{id}")]
        [ActionName("approveorder")]
        [Authorize(Policy = "AdminManagerPolicy")]
        public async Task<IActionResult> Approve(int id, Orders ord)
        {
            try
            {
                var order = await repository.GetAsync(id);
                if (order == null)
                {
                    return NotFound("Record Not found");
                }
                if (order.IsOrderApproved)
                {
                    Response.Headers.Add("Response ", "Order is already approved");
                    return NoContent();
                }
                var res = await repository.ApproveOrderAsync(id);
                return Ok(res);
            }
            catch (Exception ex)
            {
                Response.Headers.Add("Error", ex.Message);
                return NoContent();
            }
        }


        [HttpDelete("{id}")]
        [ActionName("deleteorder")]
        [Authorize(Policy = "AdminManagerPolicy")]
        public IActionResult Delete(int id)
        {
            try
            {
                var response = repository.DeleteAsync(id).Result;
                if (response) return Ok(response);
                return NotFound($"Record based on {id} is not found.");
            }
            catch (Exception ex)
            {
                Response.Headers.Add("Error", ex.Message);
                return NoContent();
            }
        }


        private void GetRequestInfo(HttpRequest request, out string userName, out string roleName)
        {
            var headers = request.Headers["Authorization"];
            var receivedToken = headers[0].Split(" ");
            userName =  service.GetUserFromTokenAsync(receivedToken[1]).Result;
            roleName = service.GetRoleFormToken(receivedToken[1]);
        }
    }
}
Listing 21: The OrdersController code     

We are applying Role Policies on action methods of the OrdersController class. The OrdersController class has the following methods
  • GetRequestInfo: This method accepts HttpRequest as input parameter and the userName and roleName as out parameters. This method reads the Authorization header from the request and then accesses the token from it. This token is passed to GetUserFromTokenAsync() and GetRoleFromTokenAsync() methods of the AuthSecurityService class and retrieves UserName and RoleName from the token. We need to do this because the OrdersControl contains action methods to Create and Update Orders methods and saved the information about which user has created and updated the order.
  • Get: This method is authorized to AdminManagerClerkPolicy, this means that it can be accessed by all users in Administrator, Manager, and Clerk roles. The method checks if the role is Administrator, if yes then all Orders will be returned otherwise orders created by the current login user will be returned.
  • Post and Put methods are accessible to Administrator, Manager, and Clerk Role users.
  • Approve and Delete: These methods are only accessible to AdminManagerPolicy roles. This means that users from Administrator and Manager roles can access this method.
So here we have implemented the roles policies to grant access to the Orders methods.
          
Run the application, the Browser will be loaded and Swagger UI will be loaded in it as shown in Figure 1



Figure 1: The Swagger UI loaded in the browser   
  
Open the SQL Server Database Explorer. In this Explorer, we will see the RBSAppDBNet6 and RBSAuthDBNet6 databases for storing Orders and ASP.NET Core Security tables respectively. Expand tables in RBSAuthDBNet6 and browse rows of the AspNetRoles table, this will show the Administrator Role as shown in Figure 2


Figure 2: The Administrator Role 

Open the AspNetUsers table, this will show the mahesh@myapp.com user as shown in figure 3. 



Figure 3: The default user

 We have created this Administrator Role and the mahesh@myapp.com user in the GlobalOps class in the AppBuilder folder. 

Visit back to the browser and in Security API, click on the /api/Security/post/auth/user link, this will be used to authenticate user. Enter the USerName and Password  as shown in figure 4


Figure 4: The Administrator Login

Click on the Execute button, the mahesh@myapp.com, Administrator login will take place and the token will be generated by the application as shown in figure 5



Figure 5: The Administrator login 

Copy this token, we will use this to authorize all actions which we will be testing using Swagger UI, we have already added the code for Token-Based Authentication for Swagger UI in listing 17. Click on the Authorize button on the top of the Swagger UI in Browser and paste the copied token in to Authorize the user as shown in figure 6


Figure 6: The Authorization using a token 

Once the Authorize button is clicked, the token will be added into the Authorization Headers in the HTTP Request as shown in figure 7


Figure 7: The Authorization header 

Click on the Close button. Since the Administrator is logged in, we can create roles for the application. Click on the /api/Security/post/role/create link and enter Name and NormalizedName values as shown in figure 8 and click on the Execute button. 


Figure 8: Creating New Role for the application

This will create a new role for the application as shown in figure 9



Figure 9: The New Role is created

Similarly, create a new Clerk Role. Once Roles are created, click on Authorize button and click on the Logout button from the dialog box displayed. This will log out the Administrator role user and the HTTP request Authorization header will be cleared.

Noe lets create new users for the application. To do that, click on the  /api/Security/post/register/user link. Here we need to enter Email, Password, and ConfirmPassword to create a new user as shown in figure 10


Figure 10: Create new user

 Click on the Execute button, this will create a new user. Similarly, create one more user of Email as user2@msit.com, Password, and ConfirmPassword as P@ssword_.  The user-created message will be responded to as shown in figure 11


Figure 11: User Creation Success 

We have created two users, let us try to authenticate using user1@msit.com user as we have seen in figure 4 as shown in figure 12


Figure 12: The user1@msit.com user login 
Click on Execute button, the response is generated is shown in figure 13


Figure 13: The User Not activated response 

Although we have the user1@msit.com registered with the application, the application is denying to authenticate the user because the user is not approved or we can say that the role is not assigned to the user. This means that the role must be assigned to the user and it can be down only by using the Administrator role user. Visit back to listing 14, here we have defined various policies to access the application. We have AdminPolicy for the Administrator role. The AdminPolicy has applied on the ActivateUserAsync() method of the SecurityController which has been seen in listing 20.  

To Approve the user1@msit.com use, login with Administrator user i.e. mahesh@myapp.com  as explained in Figures 4,5,6,7. Once the login is completed and we configure the token for Administrator with Swagger UI, click on the /api/Security/post/activate/user and add UserName and RoleName as shown in figure 14


Figure 14: The User Activation

Once the Execute button is clicked, the user will be activated as shown in figure 15


Figure 15: The User is activated   

Once the user is activated, we can log out as Administrator by clicking an Authorize button. Now, try to log in using the user1@msit.com user by clicking on the /api/Security/post/auth/user. Now we are able to log in using user1@msit.com because the Administrator has assigned a Manager role to it. The token is generated for the user1@msit.com as shown in figure 16



Figure 16: Login with user1@msit.com 

Copy this token and use this token to Authorize the user as shown in Figure 6. Since we have logged as user1@msit.com, try to access the /api/Security/roles/readall link, this will generate the error response because only the Administrator role user can access the Role information, we have already added this in listing 20 where the SecurityController contains GetRolesAsync() method which is applied with Authorize policy for as AdminPolicy. But we can use user1@msit.com for Create, Update, and Delete orders.  

This is how we can implement the Role-Based Authorization in API Application ins ASP.NET Core 6.

The Code for this article is available on this link.

Conclusion: In ASP.NET Core, the Authentication and Authorization mechanism is made really easy. With the help of Role-Based policies, we can easily provide access to API methods to users of a specific role.        
         

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