Role-Based Basic Authentication in ASP.NET Core Web API

Role-Based Basic Authentication in ASP.NET Core Web API

In this article, I will discuss how to implement Role-Based Basic Authentication in ASP.NET Core Web API Application with an example. Please read our previous article discussing on how to Implement Basic Authentication in ASP.NET Core Web API.

What is Role-Based Authentication in ASP.NET Core Web API?

Role-Based Authentication in ASP.NET Core Web API is actually a combination of two concepts: Authentication and Authorization. Authentication verifies who the user is, and authorization determines what resources a user is allowed to access. In the context of Role-Based Access Control (RBAC), the authorization process uses roles assigned to users to determine whether they have permission to access certain resources or execute specific operations.

What is Role-Based Basic Authentication in ASP.NET Core Web API?

Role-Based Basic Authentication in ASP.NET Core Web API is a mechanism that combines Basic Authentication with Role-Based Authorization. This approach is used to control access to various parts of an API based on the roles assigned to authenticated users.

  • Basic Authentication: Basic Authentication involves sending a username and password with each request, typically encoded in Base64. The server decodes this information and validates the credentials against a stored set of usernames and passwords. If the authentication is successful, the server processes the request; if not, it denies access.
  • Role-Based Authorization: In Role-Based Authorization, the authorization process uses roles assigned to users to determine whether they have permission to access certain resources or execute specific operations. Roles represent a collection of permissions or a level of access control assigned to a user. For example, an “Admin” might have permission to access all API endpoints, whereas a “User” might have limited access.
How to Implement Role-Based Basic Authorization in ASP.NET Core Web API?

Implementing Role-Based Basic Authentication in an ASP.NET Core Web API involves creating a custom authentication scheme where user credentials are validated against a store (like a database), and roles are assigned to users to enforce authorization policies on your controllers and actions. The following are the 3 steps to implement Role-Based Basic Authentication in ASP.NET Core Web API Applications:

  1. Authentication: The user provides a username and password with each API request. The API checks these credentials and considers the user authenticated if they are valid.
  2. Assigning Role Claims to User’s Identity: During the authentication process, claims are generated and associated with the user’s identity. These claims include roles that are associated with the user’s credentials (like “Admin”, “User”, “Manager”, etc.). These roles are stored as part of the user’s identity within the API.
  3. Authorization: Once authenticated, when the user attempts to access a protected resource, the API checks not only if the user is authenticated but also if their roles allow them to access that protected endpoint. This is typically done using attributes like [Authorize(Roles = “Admin”)], which specifies that only users with the “Admin” role can access the annotated endpoint.

Let us proceed and implement Role-Based Basic Authentication in an ASP.NET Core Web API Application step by step. Also, let us create a client application consuming the API Endpoints to understand how Role-Based Authentication works.

Create the User and Roles Models

Create a class file named User.cs within the Models folder and then copy and paste the following code. Here, you can see that we have created the Roles property in the User model to hold the required user roles.

namespace BasicAuthenticationDemo.Models
{
    public class User
    {
        public int Id { get; set; }
        public string Username { get; set; }
        public string Password { get; set; }
        public List<Role>? Roles { get; set; }
    }

    public class Role
    {
        public int Id { get; set; }
        public string RoleName { get; set; }
    }
}
Create the User Repository:

Next, create a class file named UserRepository.cs within the Models folder and copy and paste the following code. This will be the repository where we perform the typical database CRUD Operations on the User entity.

namespace BasicAuthenticationDemo.Models
{
    namespace BasicAuthenticationDemo.Models 
    {
        // Interface defining the contract for user repository operations.
        public interface IUserRepository
        {
            Task<User?> ValidateUser(string username, string password); // Method to validate a user's credentials.
            Task<List<User>> GetAllUsers(); // Method to retrieve all users.
            Task<User?> GetUserById(int id); // Method to fetch a single user by ID.
            Task<User> AddUser(User user); // Method to add a new user.
            Task<User> UpdateUser(User user); // Method to update an existing user's details.
            Task DeleteUser(int id); // Method to delete a user by ID.
        }

        // Concrete implementation of IUserRepository.
        public class UserRepository : IUserRepository
        {
            // In-memory list of users and the corresponding Roles.
            private List<User> users = new List<User>
            {
                new User { Id = 1, Username = "admin", Password = "admin", Roles = new List<Role> { new Role { Id = 1, RoleName = "Admin" } } },
                new User { Id = 2, Username = "user", Password = "user", Roles = new List<Role> { new Role { Id = 2, RoleName = "User" } } },
                new User { Id = 3, Username = "Pranaya", Password = "Test@1234", Roles = new List<Role> { new Role { Id = 1, RoleName = "Admin" }, new Role { Id = 2, RoleName = "User" } } },
                new User { Id = 4, Username = "Kumar", Password = "Test@123", Roles = new List<Role> { new Role { Id = 2, RoleName = "User" } }  }
            };

            // Validates user credentials against the stored list.
            public async Task<User?> ValidateUser(string username, string password)
            {
                await Task.Delay(100); // Simulates a delay, mimicking database latency.
                return users.FirstOrDefault(u => u.Username == username && u.Password == password); // Returns the user if credentials match.
            }

            // Retrieves all users.
            public async Task<List<User>> GetAllUsers()
            {
                await Task.Delay(100); // Simulates a delay.
                return users.ToList(); // Converts the list of users to a new list and returns it.
            }

            // Retrieves a user by ID.
            public async Task<User?> GetUserById(int id)
            {
                await Task.Delay(100); // Simulates a delay.
                return users.FirstOrDefault(u => u.Id == id); // Returns the user if found.
            }

            // Adds a new user if no duplicate ID is found.
            public async Task<User> AddUser(User user)
            {
                await Task.Delay(100); // Simulates a delay.
                if (users.Any(u => u.Id == user.Id))
                {
                    throw new Exception("User already exists with the given ID."); // Exception if user with same ID exists.
                }

                users.Add(user); // Adds the new user to the list.
                return user; // Returns the added user.
            }

            // Updates an existing user's details.
            public async Task<User> UpdateUser(User user)
            {
                await Task.Delay(100); // Simulates a delay.
                var existingUser = await GetUserById(user.Id); // Fetches the user by ID.
                if (existingUser == null)
                {
                    throw new Exception("User not found."); // Throws an exception if user not found.
                }

                existingUser.Username = user.Username; // Updates username.
                existingUser.Password = user.Password; // Updates password, consider hashing in production.
                return existingUser; // Returns the updated user.
            }

            // Deletes a user by ID.
            public async Task DeleteUser(int id)
            {
                await Task.Delay(100); // Simulates a delay.
                var user = await GetUserById(id); // Fetches the user by ID.
                if (user == null)
                {
                    throw new Exception("User not found."); // Throws an exception if user not found.
                }

                users.Remove(user); // Removes the user from the list.
            }
        }
    }
}
Register the User Repository:

Next, we need to register the User Repository service with the built-in dependency injection container so that we can access the service throughout the application using dependency injection. Please include the following line of code in the Program class.

builder.Services.AddSingleton<IUserRepository, UserRepository>();

Creating Role-Based Basic Authentication Handler Service:

Next, we need to create a Custom Service to implement the Role-Based Basic Authentication. So, create a class file named BasicAuthenticationHandler.cs within the Models folder and copy and paste the following code. This class is inherited from AuthenticationHandler<BasicAuthenticationOptions> and will handle the authentication process. The following code is self-explained, so please go through the comment lines for a better understanding:

using BasicAuthenticationDemo.Models.BasicAuthenticationDemo.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using System.Linq;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;

namespace BasicAuthenticationDemo.Models
{
    public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        private readonly IUserRepository _userRepository; // Dependency injection for the user repository

        // Constructor
        public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options, // Options for configuring authentication schemes
            ILoggerFactory logger, // Factory to create logger objects
            UrlEncoder encoder, // Encoder for URL to ensure safe URLs
            IUserRepository userRepository) // User repository to handle user data
            : base(options, logger, encoder) // Pass options, logger, and encoder to the base class
        {
            _userRepository = userRepository; // Initialize user repository with dependency injection
        }

        // Method to handle the authentication process
        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            // Check if the Authorization header is present
            if (!Request.Headers.ContainsKey("Authorization"))
            {
                // Fail authentication if header is missing
                return AuthenticateResult.Fail("Missing Authorization Header"); 
            }
            
            User? user;
            try
            {
                // Parse the Authorization header and validate its format
                if (!AuthenticationHeaderValue.TryParse(Request.Headers["Authorization"], out var authHeader))
                {
                    // Fail authentication if header format is wrong
                    return AuthenticateResult.Fail("Invalid Authorization Header Format");
                }
                
                // Decode the Base64 encoded credentials from the header
                var credentialBytes = Convert.FromBase64String(authHeader.Parameter ?? string.Empty);

                // Split decoded credentials into username and password
                var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':', 2);

                if (credentials.Length != 2)
                {
                    // Ensure credentials are in correct format
                    return AuthenticateResult.Fail("Invalid Authorization Header Content");
                }

                // Extract username
                var username = credentials[0];

                // Extract password
                var password = credentials[1]; 

                // Validate user against the stored credentials
                user = await _userRepository.ValidateUser(username, password);
            }
            catch (FormatException) // Handle format exceptions for Base64 decoding
            {
                return AuthenticateResult.Fail("Invalid Base64 Encoding in Authorization Header");
            }
            catch (Exception) // Handle general exceptions
            {
                return AuthenticateResult.Fail("Error Processing Authorization Header");
            }

            if (user == null)
            {
                // Check if user is not found or invalid credentials
                return AuthenticateResult.Fail("Invalid Username or Password");
            }

            // Create claims based on the valid user identifiers
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.Username)
            };

            //Adding Roles as Claims
            claims.AddRange(user.Roles.Select(role => new Claim(ClaimTypes.Role, role.RoleName)));

            // Create an identity with claims
            var identity = new ClaimsIdentity(claims, Scheme.Name);

            // Create principal from identity
            var principal = new ClaimsPrincipal(identity);

            // Create ticket with principal and scheme
            var ticket = new AuthenticationTicket(principal, Scheme.Name);

            // Return success with the authentication ticket
            return AuthenticateResult.Success(ticket); 
        }

        // Method to handle the authentication challenge
        protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
        {
            // Set the WWW-Authenticate header in the response. This instructs the client
            // (usually a web browser) to prompt the user with a login dialog for username and password.
            Response.Headers["WWW-Authenticate"] = "Basic realm=\"BasicAuthenticationDemo\", charset=\"UTF-8\"";

            // Set the HTTP status code to 401 Unauthorized to indicate that the request has failed
            // authentication and needs proper credentials to proceed.
            Response.StatusCode = 401;

            // Send a custom message in the response body. This message can be seen by the client if they
            // access the raw HTTP response. It's a clear indicator as to why the request was rejected.
            await Response.WriteAsync("You need to authenticate to access this resource.");
        }
    }
}
Configure Authentication in Program Class:

In the Program.cs file, add and configure the authentication middleware. So, please modify the Program class as follows:

using BasicAuthenticationDemo.Models;
using BasicAuthenticationDemo.Models.BasicAuthenticationDemo.Models;
using Microsoft.AspNetCore.Authentication;

namespace BasicAuthenticationDemo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddControllers()
            .AddJsonOptions(options =>
            {
                // This will use the property names as defined in the C# model
                options.JsonSerializerOptions.PropertyNamingPolicy = null;
            });

            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            builder.Services.AddSingleton<IUserRepository, UserRepository>();

            //Adding Basic Authentication Scheme
            builder.Services.AddAuthentication("BasicAuthentication")
            .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", options => { });

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            // Add Authentication to the request pipeline
            app.UseAuthentication();
            app.UseAuthorization();

            app.MapControllers();

            app.Run();
        }
    }
}
Apply Role-Based Authorization in API Controller:

Next, create an API Empty Controller named UsersController within the Controller folder and copy and paste the following code which applied Role Based Basic Authentication to protect the endpoints. The following code is self-explained, so please go through the comment lines for a better understanding:

using BasicAuthenticationDemo.Models;
using BasicAuthenticationDemo.Models.BasicAuthenticationDemo.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace BasicAuthenticationDemo.Controllers
{
    // Specifies that this class is a controller with API-specific behaviors.
    [ApiController] 
    // Sets the base route for the API controller. [controller] is replaced by the controller's name.
    [Route("api/[controller]")] 
    public class UsersController : ControllerBase
    {
        // Declares a private read-only field for the user repository interface.
        private readonly IUserRepository _userRepository; 
        
        // Constructor for UsersController, dependency injecting the IUserRepository.
        public UsersController(IUserRepository userRepository)
        {
            // Assigns the injected userRepository to the field _userRepository.
            _userRepository = userRepository; 
        }

        // Defines a GET method to retrieve all users.
        [HttpGet] 
        // Applies Basic Authentication to this method.
        [Authorize(AuthenticationSchemes = "BasicAuthentication")] 
        public async Task<ActionResult<IEnumerable<User>>> GetUsers()
        {
            // Retrieves all users from the repository asynchronously.
            var users = await _userRepository.GetAllUsers();

            // Returns the list of users with an HTTP 200 OK status code.
            return Ok(users);
        }

        // Defines a GET method to retrieve a single user by ID.
        [HttpGet("{id}")]
        // Applies Basic Authentication and restricts access to users with the "User" role.
        [Authorize(AuthenticationSchemes = "BasicAuthentication", Roles = "User")] 
        public async Task<ActionResult<User>> GetUser(int id)
        {
            var user = await _userRepository.GetUserById(id); // Retrieves a user by their ID asynchronously.
            if (user == null) // Checks if the retrieved user is null (not found).
            {
                return NotFound("User not found."); // Returns a 404 Not Found status code if the user does not exist.
            }
            return Ok(user); // Returns the found user with an HTTP 200 OK status code.
        }

        // Defines a POST method to create a new user.
        [HttpPost]
        // Restricts this method to users with the "Admin" role.
        [Authorize(AuthenticationSchemes = "BasicAuthentication", Roles = "Admin")] 
        public async Task<ActionResult<User>> CreateUser([FromBody] User user)
        {
            try
            {
                var createdUser = await _userRepository.AddUser(user); // Attempts to add a new user to the repository.
                // Returns a 201 Created status code, along with the route and data for the created user.
                return CreatedAtAction(nameof(GetUser), new { id = createdUser.Id }, createdUser);
            }
            catch (Exception ex)
            {
                return BadRequest(ex.Message); // Returns a 400 Bad Request status code if there is an exception, with the exception message.
            }
        }

        // Defines a PUT method to update an existing user.
        [HttpPut("{id}")]
        // Allows access to both "Admin" and "User" roles.
        [Authorize(AuthenticationSchemes = "BasicAuthentication", Roles = "Admin, User")] 
        public async Task<ActionResult> UpdateUser(int id, [FromBody] User user)
        {
            if (id != user.Id) // Checks if the ID in the URL matches the ID in the body.
            {
                return BadRequest("ID mismatch in the URL and body."); // Returns a 400 Bad Request if IDs do not match.
            }

            try
            {
                await _userRepository.UpdateUser(user); // Tries to update the user in the repository.
                return NoContent(); // Returns a 204 No Content status code if successful, indicating no data to return.
            }
            catch (Exception ex)
            {
                if (ex.Message == "User not found.")
                {
                    return NotFound(ex.Message); // Returns a 404 Not Found if the user does not exist.
                }
                return BadRequest(ex.Message); // Returns a 400 Bad Request for other exceptions.
            }
        }

        // Defines a DELETE method to remove a user.
        [HttpDelete("{id}")]
        // The User must have both "User" and "Admin" roles to access the below end points.
        [Authorize(AuthenticationSchemes = "BasicAuthentication", Roles = "User")]
        [Authorize(AuthenticationSchemes = "BasicAuthentication", Roles = "Admin")]
        public async Task<ActionResult> DeleteUser(int id)
        {
            try
            {
                await _userRepository.DeleteUser(id); // Tries to delete the user by ID.
                return NoContent(); // Returns a 204 No Content status code, indicating successful deletion.
            }
            catch (Exception ex)
            {
                if (ex.Message == "User not found.")
                {
                    return NotFound(ex.Message); // Returns a 404 Not Found if the user does not exist.
                }
                return BadRequest(ex.Message); // Returns a 400 Bad Request for other exceptions.
            }
        }
    }
}
Creating Client Application:

Now, we will create a Console Application named APIConsumerApp that will consume the Web API services with Role-Based Basic Authentication. So, please modify the Program class as follows. The following code is self-explained, so please go through the comment lines for a better understanding.

using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;

namespace ApiConsumerApp
{
    class Program
    {
        // Initialize a static HttpClient instance for the lifetime of the application.
        static HttpClient client = new HttpClient();

        static async Task Main(string[] args)
        {
            // Set the base address of the API.
            client.BaseAddress = new Uri("https://localhost:7215");
            
            // Clear any previously set request headers.
            client.DefaultRequestHeaders.Accept.Clear();
            
            // Add JSON to the Accept header to tell the server to send data in JSON format.
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            // Set up basic authentication for all requests.
            // Encoding the username and password as ASCII.
            // var byteArray = Encoding.ASCII.GetBytes("admin:admin");
             var byteArray = Encoding.ASCII.GetBytes("user:user");
            //var byteArray = Encoding.ASCII.GetBytes("Pranaya:Test@1234");
            // var byteArray = Encoding.ASCII.GetBytes("Kumar:Test@123");
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));

            try
            {
                // Perform a GET request to retrieve all users.
                Console.WriteLine("Getting all users...");
                var users = await GetAsync("api/users");
                Console.WriteLine(users);

                // Perform a GET request to retrieve all users.
                Console.WriteLine("Getting user by Id...");
                var user = await GetAsync("api/Users/1");
                Console.WriteLine(user);

                // Perform a POST request to create a new user.
                Console.WriteLine("Creating a new user...");
                var newUser = new { Id = 5, Username = "newuser", Password = "newpassword" };
                var postResult = await PostAsync("api/users", newUser);
                Console.WriteLine(postResult);

                // Perform a PUT request to update a user.
                Console.WriteLine("Updating a user...");
                var updatedUser = new { Id = 1, Username = "updateduser", Password = "updatedpassword" };
                var putResult = await PutAsync("api/users/1", updatedUser);
                Console.WriteLine(putResult);

                // Perform a DELETE request to remove a user.
                Console.WriteLine("Deleting a user...");
                var deleteResult = await DeleteAsync("api/users/1");
                Console.WriteLine(deleteResult);

                // Wait for a key press before closing to see the results.
                Console.ReadKey();
            }
            catch (Exception e)
            {
                // Output any exceptions to the console.
                Console.WriteLine(e.Message);
            }
        }

        // Method for GET requests.
        static async Task<string> GetAsync(string path)
        {
            HttpResponseMessage response = await client.GetAsync(path);
            // Check if the request was successful and read the response content as a string.
            return response.IsSuccessStatusCode ? await response.Content.ReadAsStringAsync() : $"Error: {response.StatusCode}";
        }

        // Method for POST requests.
        static async Task<string> PostAsync(string path, object value)
        {
            HttpResponseMessage response = await client.PostAsJsonAsync(path, value);
            // Check if the request was successful and read the response content as a string.
            return response.IsSuccessStatusCode ? await response.Content.ReadAsStringAsync() : $"Error: {response.StatusCode}";
        }

        // Method for PUT requests.
        static async Task<string> PutAsync(string path, object value)
        {
            HttpResponseMessage response = await client.PutAsJsonAsync(path, value);
            // Return a success message or an error message depending on the request status.
            return response.IsSuccessStatusCode ? "Updated successfully." : $"Error: {response.StatusCode}";
        }

        // Method for DELETE requests.
        static async Task<string> DeleteAsync(string path)
        {
            HttpResponseMessage response = await client.DeleteAsync(path);
            // Return a success message or an error message depending on the request status.
            return response.IsSuccessStatusCode ? "Deleted successfully." : $"Error: {response.StatusCode}";
        }
    }
}
Testing the Application:

First, run the ASP.NET Core Web API Application, get the Port number on which the Web API Application is running. Then update the Port number in the Console Application where we set the base URL and then run the Console Application. You should get the following output. Now, you can run the application with different user having different roles and see whether the Role Bases Basic Authentication is working as expected or not.

How to implement Role-Based Basic Authentication in ASP.NET Core Web API Application

What are the Advantages and Disadvantages of Role-Based Basic Authentication in ASP.NET Core Web API?

Role-Based Basic Authentication in ASP.NET Core Web API offers a mix of benefits and drawbacks, making it suitable for some scenarios but less ideal for others. The following is a detailed look at the advantages and disadvantages of Role-Based Basic Authentication:

Advantages of Role-Based Basic Authentication in ASP.NET Core Web API
  • Simplicity: Basic authentication is one of the simplest forms of authentication. It doesn’t require complex infrastructure or third-party services, which can be ideal for small applications or internal systems where simplicity is a priority.
  • Immediate Role Recognition: Integrating roles into the Basic Authentication process allows the system to recognize a user’s roles upon authentication immediately. As a result, the API can easily control access based on these roles.
Disadvantages of Role-Based Basic Authentication in ASP.NET Core Web API
  • Security Concerns: Basic Authentication transmits credentials in an encoded form (Base64) but is not encrypted, making it vulnerable to interception if not used with HTTPS. Even with HTTPS, the credentials are repeatedly sent with each request, increasing the exposure risk.
  • No Built-In Support for Token-Based Features: Unlike modern authentication mechanisms such as JWT, Basic Authentication does not support token expiration, revocation, or information embedding (like user roles or permissions directly within the token).
  • Limited Use Cases: Due to its simplicity and security limitations, Basic Authentication is generally not recommended for public-facing applications, especially those handling sensitive data or requiring robust security features.
Better Alternatives of Basic Authentication in ASP.NET Core Web API

Token-Based Authentication: Modern applications often use token-based authentication (such as JWT), which can encapsulate user identity and the roles in a secure token, providing better security. 

In the next article, I will discuss how to implement JWT Authentication in ASP.NET Core Web API Application. In this article, I explain Role-Based Basic Authentication in an ASP.NET Core Web API Application with an Example. I hope you enjoy this ASP.NET Core Web API Role-Based Basic Authentication article.

Leave a Reply

Your email address will not be published. Required fields are marked *