ASP.NET Core 9: Implementing Secure API with Policy Based Authorization and Token Based Authentication
Implementing the secure APIs is one of the most needed features of the modern applications. In the current era of the application development where APIs are used for data communication across homogeneous and heterogeneous platform application the challenge is to implement the secure communication with APIs. Thanks to ASP.NET Core for providing robust but yet easy mechanism of securing API.
Token Based Authentication
Token-based authentication is a method where users verify their identity by receiving a unique access token. This token is then used to access resources without needing to re-enter credentials each time. The Token based authentication works as follows:
User Authentication:
- The user logs in with their credentials (e.g., username and password).
- The server verifies the credentials and, if valid, generates a token.
- 2. Token Issuance
- The server issues a token, which is a string that encodes the user's identity and any claims like UserName, Roles or Permissions.
- This token is sent back to the client.
- The client stores the token (typically in local storage or a cookie).
- For subsequent requests, the client includes the token in the HTTP headers in the Authorization header.
- 4. Token Validation:
- The server validates the token on each request to ensure it is still valid and has not expired.
- If the token is valid, the server processes the request; otherwise, it returns an error (e.g., 401 Unauthorized or 403 Forbidden).
Figure 1: The Token Based Authentication
The implementation
For the implementation the SQL Server, .NET 9 and Visual Studio 2022 is mandatory. We will be using the Entity Framework Core (EF Core) code-first approach for creating ASP.NET Core Identity Database and Tables where we will store Users, Roles, UserRoles, etc. We will also use a separate database where we will be creating Order Sales Database that contains Orders table. This database and table will also be created using the code-first approach.
Step 1: Open Visual Studio 2022 and create a ASP.NET Core API project targeted to .NET 9. Name this project as Core_RBS_Tokens. In this project add following packages so that we can use EF Core, ASP.NET Core Identity, and JSON Web Tokens.
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.Design
- Microsoft.EntityFrameworkCore.Relational
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
- Microsoft.AspNetCore.Identity.EntityFrameworkCore
- Microsoft.AspNetCore.Authorization
- Microsoft.AspNetCore.Authentication.JwtBearer
Step 2: In the project add a new Models folder. In this folder, add a new class file named SecurityModels.cs. In this file we will add classed for User registration, Login, Role Creation, UserRole to assign role to user and sending the Response to client with Token Details. Listing 1 shows the code for these classes.
using System.ComponentModel.DataAnnotations; namespace Core_RBS_Tokens.Models { public class RegisterUser { /// <summary> /// UNique EMail /// </summary> [Required(ErrorMessage = "Email is Must")] [EmailAddress] public string? Email { get; set; } [Required(ErrorMessage = "Password is Must")] [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 must be string password with uppercase character, number and sepcial character")] public string? Password { get; set; } [Compare("Password")] public string? ConfirmPassword { get; set; } } public class LoginUser { [Required(ErrorMessage ="User Name is Must")] public string? Email { get; set; } [Required(ErrorMessage = "Password is Must")] 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? RoleName { 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;} } public class Users { public string? Email { get; set; } public string? UserName { get; set; } } }
Listing 1: Security Classes
In the Models folder, add a new class file named SecurityDbContext.cs. In this class file we will create a DbContext class that will be used in the EF Core Migration. Listing 2 shows the code for SecurityDbContext class.
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace Core_RBS_Tokens.Models { public class SecurityDbContext : IdentityDbContext { public SecurityDbContext(DbContextOptions<SecurityDbContext> options):base(options) { } } }
Listing 2: The SecurityDbContext class
"ConnectionStrings": { "SecurityConnStr": "Data Source=.;Initial Catalog=SecurityDBArt;Integrated Security=SSPI;TrustServerCertificate=True", ....... },
Listing 3: The Connection String in the appsettings.json
Modify the Program.cs to register the SecurityDbContext class in Dependency Container as shown in Listing 4.
..... builder.Services.AddDbContext<SecurityDbContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("SecurityConnStr")); }); .....
Listing 4: Registration of SecurityDbContext class
Navigate to the Command Prompt and navigate to the current project folder. Run following commands to generate database migrations and updating the database.
dotnet ef migrations add securityMigration -c Core_RBS_Tokens.Models
dotnet ef database update -c Core_RBS_Tokens.Models
Now we have generated the ASP.NET Core Identity Tables in SQL Server database, it's time to create application database and tables for sales.
Step 3: In the Models folder, add a new class file named Order.cs. In this class file we will add code for Order, Item, and ItemsDb classes. In the ItemsDb class we will create hard-coded list for items. Listing 5 shows code for these classes.
using System.ComponentModel.DataAnnotations; using System.Security.Principal; namespace Core_RBS_Tokens.Models { public class Item { public string? ItemName { get; set; } public double UnitPrice { get; set; } } public class ItemsDB:List<Item> { public ItemsDB() { Add(new Item { ItemName="Laptop", UnitPrice=123000 }); Add(new Item { ItemName="Mobile", UnitPrice=23000 }); Add(new Item { ItemName="Charger", UnitPrice=3000 }); Add(new Item { ItemName="Charger Cable", UnitPrice=1200 }); Add(new Item { ItemName="USB", UnitPrice=1000 }); Add(new Item { ItemName="Power Bank", UnitPrice=10000 }); Add(new Item { ItemName="Laptop Charger", UnitPrice=15000 }); Add(new Item { ItemName="Screen", UnitPrice=8000 }); Add(new Item { ItemName="Router", UnitPrice=3000 }); } } public class Order { public int OrderId { get; set; } [Required(ErrorMessage ="Customer Name is Required")] public string? CustomerName { get; set; } [Required(ErrorMessage ="Item Name is Required")] public string? ItemName { get; set; } [Required(ErrorMessage = "Date is Required")] public DateOnly OrderedDate { get; set; } [Required(ErrorMessage ="Quantity is Required")] public int Quantity { get; set; } public double TotalPrice { get; set; } [Required(ErrorMessage ="Order Status is Mandatory")] public string? OrderStatus { get; set; } public string? CreatedBy { get; set; } public string? UpdatedBy { get; set; } public DateOnly UpdatedDate { get; set; } public bool IsApproved { get; set; } [Required(ErrorMessage ="Comments is required")] public string? Comments { get; set; } } }
Listing 5: The Application classes
In the Models folder, add a new class file named SalesContext.cs. In this class file we will add code for SalesContext class where we will define mapping for Order class so that using EF Core code-first approach we will create database and tables for sales. Listing 6 shows code for SalesContext class.
using Microsoft.EntityFrameworkCore; namespace Core_RBS_Tokens.Models { public class SalesContext : DbContext { public SalesContext(DbContextOptions<SalesContext> options):base(options) { } public DbSet<Order>? Orders { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); } } }
Listing 6: The SalesContext class
Modify the appsettings.json file to define SQL Connection string so that the database is created. Listing 7 shows the Connection string.
....... "AppConnStr": "Data Source=.;Initial Catalog=SalesDbArt;Integrated Security=SSPI;TrustServerCertificate=True" .......
Listing 7: The appsettings.json file
Modify the Program.cs to register the SalesContext class in dependency container as shown in Listing 8.
builder.Services.AddDbContext<SalesContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("AppConnStr")); });
Listing 8: The SalesContext class registration
As shown in the step 2, use migrations and update commands to create database for Sales and its tables.
Step 4: To standardize the response from API endpoints, we will create a ResponseObject class by adding a new file in Models folder named ResponseObject.cs. Listing 9, shows code for the ResponseObject class.
namespace Core_RBS_Tokens.Models { public class ResponseObject<TEntity> where TEntity : class { public IEnumerable<TEntity>? Records { get; set; } public TEntity? Record { get; set; } public string? Message { get; set; } public int StatusCode { get; set; } } }
Listing 9: The ResponseObject class
Step 5: Modify the Program.cs file to register the IdentityUser and IdentityRole classed in dependency container. These classes will be used to register resolve following classes for identity:
- UserManager: The class use for ASP.NET Core User Management. This class creates new user for the application and also used to assign new role for the user. This class also provides functionalities for finding user based on name, id, etc.
- RoleManager: The class used for Role Mangement for the application. This class provides functionality to create new role and also read roles for the application.
- SignInManager: The class used for User Login
Listing 10 shows the registration for IdentityUser and IdentityRole classes.
....... builder.Services.AddIdentity<IdentityUser, IdentityRole>() // Use the EF Core for Creating, Managing Users and Roles .AddEntityFrameworkStores<SecurityDbContext>().AddDefaultTokenProviders(); .......
Listing 10: Registration of IdentityUser and IdentityRole classes
Step 6: In the project, add a new folder named Services. In this folder, add a new class file named SecurityServices.cs. In this class file we will add code for SecurityServices class. This class will contain methods for registering new user, authenticating the user, create role, add role to user, get all users, get all roles, and get user and role name for the token. The code for SecurityServices class is shown in Listing 11.
using Core_RBS_Tokens.Models; using Microsoft.AspNetCore.Identity; using Microsoft.IdentityModel.Tokens; using System.Data; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; namespace Core_RBS_Tokens.Services { /// <summary> /// This class is used to create Users and Roles and manage Authentication using the Token /// After Generating Toking /// </summary> public class SecurityServices { UserManager<IdentityUser> _userManager; SignInManager<IdentityUser> _signInManager; RoleManager<IdentityRole> _roleManager; IConfiguration _config; 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 if (string.IsNullOrEmpty(user.Email)) { response.StatucCode = 500; response.Message = "Email is not provided"; } 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 if (string.IsNullOrEmpty(user.Email)) { response.StatucCode = 500; response.Message = "Email is not provided"; } else if (string.IsNullOrEmpty(user.Password)) { response.StatucCode = 500; response.Message = "Password is not provided"; } 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 { var authStatus = await _signInManager.PasswordSignInAsync(user.Email, user.Password, false, lockoutOnFailure: true); if (authStatus.Succeeded) { #region Logic for Generating Token var secretKeyString = _config["JWTCoreSettings:SecretKey"]; if (string.IsNullOrEmpty(secretKeyString)) { response.StatucCode = 500; response.Message = "Secret key is not configured properly"; return response; } var secretKey = Convert.FromBase64String(secretKeyString); var expiryTimeSpan = Convert.ToInt32(_config["JWTCoreSettings:ExpiryInMinuts"]); IdentityUser usr = new IdentityUser(user.Email); var claims = new[] { new Claim(ClaimTypes.Name, usr.UserName), new Claim(ClaimTypes.Role, roles[0]) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKeyString)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: null, audience: null, claims: claims, expires: DateTime.Now.AddMinutes(60), signingCredentials: creds); response.Token = new JwtSecurityTokenHandler().WriteToken(token); #endregion response.StatucCode = 200; response.RoleName = roles[0]; response.UserName = user.Email; 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 || string.IsNullOrEmpty(role.RoleName)) { 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 || string.IsNullOrEmpty(userRole.RoleName) || string.IsNullOrEmpty(userRole.UserName)) { 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 ?? string.Empty); 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; } public async Task<List<RoleData>> GetRolesAsync() { List<RoleData> roles = new List<RoleData>(); roles = await Task.Run(() => (from r in _roleManager.Roles.ToList() select new RoleData() { RoleName = r.Name, }).ToList()); return roles; } public async Task<List<Users>> GetUsersAsync() { List<Users> users = await Task.Run(() => (from u in _userManager.Users.ToList() select new Users() { Email = u.Email, UserName = u.UserName }).ToList()); return users; } public string[] GetUserNameAndRoleFromToken(HttpContext httpContext) { string[] data = new string[2]; if (httpContext.User.Identity is ClaimsIdentity identity) { var username = identity.FindFirst(ClaimTypes.Name)?.Value ?? string.Empty; var role = identity.FindFirst(ClaimTypes.Role)?.Value ?? string.Empty; data[0] = username; data[1] = role; return data; } return data; } } }
Listing 11: The SecurityServices class
The SecurityServices class from Listing 11 has the following specifications:
- The constructor is injected with UserManager, SignInManager, RoleManager classes and IConfiguration interface. The IConfiguration interface is used to read JSON Web Token settings from the appsettings.json file.
- The RegisterUserAsync() method is used to create new user for the application. The user will be created based on an Email. If the user already exists, then error message will be returned by the method using SecurityResponse class else the user will be created.
- The CreateRoleAsync() method will be used to create new role for the application. If the role already exists, then error message will be returned else role will be created.
- The AuthUser() method will be used to authenticate the user only if the role is assigned to the user. If the role is assigned to the user and the credential information (Email and Password) is correct, then the JSON Web Token (JWT) will be issued. The Token is created using the claims that contains UserName and RoleName. The expiry for the token is 60 minutes.
- The AddRoleToUserAsync() method is used to assign role to the user so that the user can access the application.
- The GetRolesAsync() and GetUserAsync() method are used to read all roles and users for the application.
- The GetUserNameAndRoleFromToken() method us used to extract username and role name from the token based on the claim types.
- The Claim class is used to represent a claim, which is a key-value pair that provides information about the user. Claims are used extensively in authentication and authorization processes to convey user identity and permissions.
- The SymmetricSecurityKey class is used to represent a symmetric security key. This class is commonly used in scenarios involving token-based authentication, such as JWT (JSON Web Token) authentication.
- The SigningCredentials class is used to specify the cryptographic key and security algorithms that are used to generate a digital signature. This is particularly important in scenarios involving token-based authentication, such as JWT (JSON Web Token) authentication.
- The JwtSecurityToken class is used to represent a JSON Web Token (JWT). This class is essential for creating, parsing, and validating JWTs in .NET applications.
- The JwtSecurityTokenHandler class is used to create, read, and validate JSON Web Tokens (JWTs). This class is essential for handling JWTs in .NET applications, especially for authentication and authorization purposes.
- The SymmetricSecurityKey class is used to represent a symmetric security key. This class is commonly used in scenarios involving token-based authentication, such as JWT (JSON Web Token) authentication.
Step 7: In the Services folder, add a new class file named AdminCreatorService.cs. In this class file we will add code for AdminCreatorService class. We will add the code in this class to create a default Administrator role and admin user. We are doing this because we want the Administrator role and admin user to be created when the application is executed for the first time. With this approach we have the application administrator ready so that this admin user in Administrator role will be used to create other new roles, assign role to user, get all roles and users for the application. This step avoids creating the Administrator role and admin user manually in the identity database. Listing 12 shows the code for the AdminCreatorService class.
using Microsoft.AspNetCore.Identity; namespace Core_RBS_Tokens.Services { /// <summary> /// The Following class createa an Administrator Role and User /// </summary> public static class AdminCreatorService { 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("admin@myapp.com"); if (user == null) { var defaultUser = new IdentityUser() { UserName = "admin@myapp.com", Email = "admin@myapp.com" }; var regUser = await userManager .CreateAsync(defaultUser, "P@ssw0rd_"); await userManager .AddToRoleAsync(defaultUser, "Administrator"); } } catch (Exception ex) { var str = ex.Message; } } } }
Listing 12: AdminCreatorService class
This is one of the most important steps of the application.
Step 8: Once we have the user and the role management logic ready with us it is time to implement the logic code. Remember we have created the Sales Database and the Order table using the EF Core code-first approach not it is time for us to implement logic for order management. In the Services folder, add a new class file named SalesService.cs in this class file we will write code for SalesService class. This class contains methods for performing Read/Write operations for the Orders table. Listing 13 shows code for the SalesService class.
using Core_RBS_Tokens.Models; using Microsoft.EntityFrameworkCore; namespace Core_RBS_Tokens.Services { public class SalesService(SalesContext context) { Item item = new Item(); ItemsDB items = new ItemsDB(); ResponseObject<Order> response = new ResponseObject<Order>(); public async Task<ResponseObject<Order>> GetAsync() { try { response.Records = await context.Orders.ToListAsync(); response.Message = "Orders Read Successfully"; response.StatusCode = 200; } catch (Exception ex) { throw ex; } return response; } public async Task<ResponseObject<Order>> GetAsync(int id) { try { response.Record = await context.Orders.FindAsync(id); response.Message = $"Order Based on id: {id} is Read Successfully"; response.StatusCode = 200; } catch (Exception ex) { throw ex; } return response; } public async Task<ResponseObject<Order>> SaveOdreAsync(Order order) { try { // Calculate the TotalPrice for the Order based on the Quantity and the // UnitPrice of the Item var unitprice = items.Where(i => i.ItemName.Trim() == order.ItemName.Trim()).FirstOrDefault().UnitPrice; order.TotalPrice = order.Quantity * unitprice; order.OrderedDate = DateOnly.FromDateTime(DateTime.Now); order.OrderStatus = "New Order"; var entity = await context.Orders.AddAsync(order); await context.SaveChangesAsync(); response.Message = $"Order is placed Successfully"; response.StatusCode = 201; } catch (Exception ex) { throw ex; } return response; } public async Task<ResponseObject<Order>> UpdateOdreAsync(int id, Order order) { try { Order? orderToUpdate = await context.Orders.FindAsync(id); orderToUpdate.ItemName = order.ItemName; order.Quantity = orderToUpdate.Quantity; order.OrderStatus = orderToUpdate.OrderStatus; orderToUpdate.UpdatedBy = order.UpdatedBy; order.UpdatedDate = DateOnly.FromDateTime(DateTime.Now); order.OrderStatus = "Updated"; // Calculate the TotalPrice for the Order based on the Quantity and the // UnitPrice of the Item var unitprice = items.Where(i => i.ItemName.Trim() == order.ItemName.Trim()).FirstOrDefault().UnitPrice; order.TotalPrice = order.Quantity * unitprice; var entity = await context.Orders.AddAsync(order); await context.SaveChangesAsync(); response.Message = $"Order is updated Successfully"; response.StatusCode = 201; } catch (Exception ex) { throw ex; } return response; } public async Task<ResponseObject<Order>> DeleteOrderAsync(int id) { try { Order? orderToDelete = await context.Orders.FindAsync(id); context.Orders.Remove(orderToDelete); await context.SaveChangesAsync(); response.Message = $"Order is deleted Successfully"; response.StatusCode = 201; } catch (Exception ex) { throw ex; } return response; } public async Task<ResponseObject<Order>> ApproveRejectOrderAsync(int id, Order order) { try { Order? orderToProcess = await context.Orders.FindAsync(id); orderToProcess.IsApproved = order.IsApproved; orderToProcess.Comments = order.Comments; await context.SaveChangesAsync(); if(orderToProcess.IsApproved) { response.Message = $"Order {id} is apporved successfully"; } else { response.Message = $"Order {id} is rejected "; } response.StatusCode = 201; } catch (Exception ex) { throw ex; } return response; } } }
Listing 13: The SalesService class
The method ApproveRejectOrderAsync() method is used to either approve or reject order rest all other methods are self-explanatory.
Step 9: In the appsettings.json file lets add settings for generating JSON Web Tokens. We need Secret Key and Expiry for the token. We can create secret key using the Cryptography features of .NET. Listing 14 shows the settings in the appsettings.json file.
...... "JWTCoreSettings": { "SecretKey": "f4LZOS1MJ+lwLI+NZDSatxQffwf4CMnCUyAJaEcd/tm5tcLhXkuV9bO+bYF+NgdmrJqE69LDDiQotz0rQIfJqw==", "ExpiryInMinuts": 20 } .....
Listing 14: The JWT Settings
Step 10: In the Program.cs, let's add the code for defining CORS policies, and registering the SecurityServices and SalesService classed in dependency container as shown in Listing 15.
....... #region The CORS builder.Services.AddCors(options => { options.AddPolicy("cors", (policy) => { policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); }); }); #endregion builder.Services.AddScoped<SecurityServices>(); builder.Services.AddScoped<SalesService>(); .......
Listing 15: The CORS policy and dependency registration
Below the registration of security and sales services classes, let's define the Authentication Service and Authorization policies. We will define AdminiPolicy for Administrator role, AdminManagerPolicy for Administrator and Manager role, and AdminManagerClerkPolicy for Administrator, Manager and Clerk roles. Listing 16 shows the policy declaration.
............... #region Define Policies builder.Services.AddAuthentication(); builder.Services.AddAuthorizationBuilder() .AddPolicy("AdminPolicy", policy => { policy.RequireRole("Administrator"); }) .AddPolicy("AdminManagerPolicy", policy => { policy.RequireRole("Administrator", "Manager"); }) .AddPolicy("AdminManagerClerkPolicy", policy => { policy.RequireRole("Administrator", "Manager", "Clerk"); });
Listing 16: The Authentication Service and Authorization Policies
We need to define the Token Validation based on the validation parameters so that when the client sent the token in the HTTP request it must read and validated based on the secret key. Listing 17 shows code for the Token Validation.
#region Token Validation // Read the Secret Key from the appsettings.json var secretKeyString = builder.Configuration["JWTCoreSettings:SecretKey"]; if (string.IsNullOrEmpty(secretKeyString)) { throw new InvalidOperationException("Secret key is not configured properly."); } byte[] secretKey = Convert.FromBase64String(secretKeyString); // set the Authentication Scheme builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = null, ValidAudience = null, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKeyString)) }; }); #endregion ..................
Listing 17: The Token Validation
Below the token validation code, let's add the JSON Serialization options for the JSON response returned from the API in pascal case as shown in Listing 18.
..................... #region The JSON Serialization builder.Services.Configure<JsonOptions>(options => { options.SerializerOptions.PropertyNamingPolicy = null; // Use Pascal case options.SerializerOptions.DictionaryKeyPolicy = null; // Use Pascal case options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); #endregion ................
Listing 18: The JSON Serialization code
In the Program.cs below the HttpsRedirection middleware add the middleware for CORS, Authentication, and Authorization. Below these middlewares access the CreateApplicationAdministrator() method from the AdminCreatorService class so that the default Administrator role and admin user is created. Make sure that the order must be followed accurately. Listing 19 shows the code for the Middleware registration.
.................. app.UseCors("cors"); app.UseAuthentication(); app.UseAuthorization(); #region The code for the Accessing Code for Creating Administrator User and Role using (var scope = app.Services.CreateScope()) { var serviceProvider = scope.ServiceProvider; await AdminCreatorService.CreateApplicationAdministrator(serviceProvider); } #endregion ..................
Listing 19: The Middlewares for Authentication and Administration role creator
So far, we have defined authorization policies and registered the Authentication and Authorization middlewares in the application pipeline. Finally, we need to add the API endpoints for Role Based Security and Order management.
Step 11: In the Program.cs let's as API Endpoints as follows
- createuser, endpoint will be accessed anonymously.
- createrole, approveuser, users, and roles endpoints will be accessed by roles in AdminPolicy.
- orders, createorder endpoint will be accessed by roles in AdminManagerClerkPolicy.
- updateorder and deleteorder endpoint will be accessed by roles in AdminManagerPolicy.
- processororder endpoint will be accessed by roles in AdminPolicy
......................... #region APIs // Create New User app.MapPost("/api/createuser", async (SecurityServices serv, RegisterUser user) => { var response = await serv.RegisterUserAsync(user); return Results.Ok(response); }); //Create New Role app.MapPost("/api/createrole", async (SecurityServices serv, RoleData role) => { var response = await serv.CreateRoleAsync(role); return Results.Ok(response); }).WithOpenApi().RequireAuthorization("AdminPolicy"); // Assign Role to User app.MapPost("/api/approveuser", async (SecurityServices serv, UserRole userrole) => { var response = await serv.AddRoleToUserAsync(userrole); return Results.Ok(response); }).RequireAuthorization("AdminPolicy"); // Authenticate the User app.MapPost("/api/authuser", async (SecurityServices serv, LoginUser user) => { var response = await serv.AuthUser(user); return Results.Ok(response); }); // Get all Users app.MapGet("/api/users", async (SecurityServices serv) => { var users = await serv.GetUsersAsync(); return Results.Ok(users); }).RequireAuthorization("AdminPolicy"); // Get All Roles app.MapGet("/api/roles", async (SecurityServices serv) => { var roles = await serv.GetRolesAsync(); return Results.Ok(roles); }).RequireAuthorization("AdminPolicy"); app.MapGet("/api/orders", async (HttpRequest request, SecurityServices serv, SalesService sales) => { // If the User is adminstrator then return all orders else return orders created by the current Login User GetRequestInfo(request, serv, out string userName, out string roleName); var orders = await sales.GetAsync(); if (orders?.Records == null) { return Results.NotFound("No orders found."); } if (roleName == "Administrator") { return Results.Ok(orders.Records); } var responseByUser = orders.Records.Where(order => order.CreatedBy?.Trim() == userName); return Results.Ok(responseByUser); }).RequireAuthorization("AdminManagerClerkPolicy"); app.MapGet("/api/orders/{id}", async (HttpRequest request, SecurityServices serv, SalesService sales, int id) => { GetRequestInfo(request, serv, out string userName, out string roleName); var orders = await sales.GetAsync(); if (orders?.Records == null) { return Results.NotFound("No orders found."); } var responseByUser = orders.Records.Where(order => order.CreatedBy?.Trim() == userName && order.OrderId == id).FirstOrDefault(); return Results.Ok(responseByUser); }).RequireAuthorization("AdminManagerClerkPolicy"); app.MapPost("/api/createorder", async (SalesService serv, Order order) => { var response = await serv.SaveOdreAsync(order); return Results.Ok(response); }).RequireAuthorization("AdminManagerClerkPolicy"); app.MapPut("/api/updateorder/{id}", async (SalesService serv, int id, Order order) => { var response = await serv.UpdateOdreAsync(id, order); return Results.Ok(response); }).RequireAuthorization("AdminManagerPolicy"); app.MapDelete("/api/deleteorder/{id}", async (SalesService serv, int id) => { var response = await serv.DeleteOrderAsync(id); return Results.Ok(response); }).RequireAuthorization("AdminManagerPolicy"); app.MapPost("/api/processorder/{id}", async (SalesService serv, int id, Order order) => { var response = await serv.ApproveRejectOrderAsync(id, order); return Results.Ok(response); }).RequireAuthorization("AdminPolicy"); #endregion void GetRequestInfo(HttpRequest request, SecurityServices serv, out string userName, out string roleName) { var headers = request.Headers["Authorization"]; var receivedToken = headers[0].Split(" "); var authDetails = serv.GetUserNameAndRoleFromToken(request.HttpContext); userName = authDetails[0]; roleName = authDetails[1]; } app.Run();
Listing 20: The APIs
So now we have created the application, lets test it. Use the Postman or AdvancedRESTClient (ARC) tool to test the application. I have used the ARC. Run the Application. Figure 2 shows the new user creation.
Figure 2: The New User Creation
The new user named tejas@myapp.com is created but the role is not assigned to the user. Let's try to login using this new user, we will get the result as shown in Figure 3
Figure 3: The User Cannot Login because the role is not assigned to the user
To assign the role to user, the user with Administrator role must be login. We have admin@myapp.com user in Administrator role. Let's login using the admin@myapp.com user. We will get the TOKEN as shown in Figure 4.
Figure 4: The Administrator login
Now let's assign Manager role to the tejas@myapp.com by accessing the approveuser endpoint as shown in Figure 5. We need to pass the Token in the request header as shown in Figure 5.
Figure 5: The User Approval
In the request body for the approve user we need to pass the RoleName as Manager as shown in Figure 6.
Figure 6: The Request body for authuser endpoint
Now try to login using the tejas@myapp.com user, you will be able to login. Now since the user tejas@myapp.com is Manager, we can create order using the login of this user as shown in Figure 7.
Figure 7: The Create Order
Now let's try to approve order by accessing the processorder endpoint using the user tejas@myapp.com, this is Manager role but the processorder endpoint can be accessed using the Administrator role. So, if we make call using the Manager role, we will get the error as shown in Figure 8.
Figure 8: Processororder endpoint error
Thats's it. The code for this article can be downloaded from this link.
Conclusion: The ASP.NET Core 9 Token based authentication and Policy based authorization is superb.