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 an ASP.NET Core Web API Application with an example. Please read our previous article, which discusses implementing Basic Authentication in ASP.NET Core Web API. Role-Based Authentication in ASP.NET Core Web API is 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.

What is Role-Based Basic Authentication?

Role-Based Basic Authentication is a security mechanism where users authenticate themselves using Basic Authentication (username and password, typically sent in the HTTP Authorization header). After authentication, the system authorizes users to access resources based on their assigned roles, such as Admin, User, Manager, etc.

  • Basic Authentication is a simple authentication scheme built into HTTP where credentials are sent as a base64-encoded string.
  • Role-Based Authorization checks what roles a user belongs to and grants or denies access to API endpoints based on those roles.
How Does Role-Based Basic Authentication Work?

Let us understand how Role-Based Basic Authentication Works. Please refer to the following diagram for a clearer understanding.

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

It will work as follows:
  • Client Request: The client sends an HTTP request with the Authorization header containing a Basic Authentication token (base64-encoded username and password).
  • Authentication: The server decodes and validates the credentials.
  • Role Retrieval: After authentication, the server retrieves the user’s roles from a data source (e.g., a database).
  • Authorization: The server checks if the user’s roles permit access to the requested API resource.
    1. If authorized, the server processes the request.
    2. If unauthorized, the server returns an HTTP 401 Unauthorized or 403 Forbidden response.
How to Implement Role-Based Basic Authentication in ASP.NET Core Web API?

Let us proceed and see how to implement Role-Based Basic Authentication in ASP.NET Core Web API step by step. We will be working with the same example that we created in our previous article.

Include Roles as Claims

Ensure the user roles are included as claims in your BasicAuthenticationHandler. We have already added the claims. The following is our BasicAuthenticationHandler class.

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

namespace BasicAuthenticationDemo.Models
{
    // Custom authentication handler that processes Basic Authentication.
    // Inherits from AuthenticationHandler<TOptions> where TOptions is AuthenticationSchemeOptions.
    public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        // Database context to query user information during authentication.
        private readonly ApplicationDbContext _context;

        // Constructor injects dependencies required by the base handler and adds DbContext.
        // parameters:
        // - IOptionsMonitor<AuthenticationSchemeOptions> options: Monitors changes to authentication scheme options
        // - ILoggerFactory logger: Factory to create logger instances
        // - UrlEncoder encoder: Encodes URLs to ensure they are safe
        // - ApplicationDbContext context: The context used to validate user credentials.
        public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ApplicationDbContext context)
            : base(options, logger, encoder)
        {
            // Assign the provided ApplicationDbContext to the private field
            _context = context;
        }

        // Core method that performs authentication logic on each request.
        // Called automatically when ASP.NET Core needs to authenticate a request.
        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            try
            {
                // Step 1: Check if the Authorization header exists in the request.
                if (!Request.Headers.ContainsKey("Authorization"))
                {
                    // No credentials provided, authentication fails.
                    return AuthenticateResult.Fail("Missing Authorization Header");
                }

                // Step 2: Retrieve the Authorization header value.
                var authorizationHeader = Request.Headers["Authorization"].ToString();

                // Step 3: Parse the header to separate scheme and parameter.
                // The expected format is: "Basic base64encodedCredentials"
                if (!AuthenticationHeaderValue.TryParse(authorizationHeader, out var headerValue))
                {
                    // If parsing fails, the header is considered invalid and authentication fails.
                    return AuthenticateResult.Fail("Invalid Authorization Header");
                }

                // Step 4: Verify that the scheme is "Basic" (case-insensitive).
                if (!"Basic".Equals(headerValue.Scheme, StringComparison.OrdinalIgnoreCase))
                {
                    // If not Basic scheme, authentication is not applicable.
                    return AuthenticateResult.Fail("Invalid Authorization Scheme");
                }

                // Step 5: Decode the Base64-encoded credentials ("username:password").
                var credentialBytes = Convert.FromBase64String(headerValue.Parameter!);
                var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':', 2);

                // Step 6: Validate that the decoded string contains exactly username and password.
                if (credentials.Length != 2)
                {
                    // If not, the credentials are invalid and authentication fails.
                    return AuthenticateResult.Fail("Invalid Authorization Header");
                }

                // Step 7: Extract username (here Email) and password from credentials.
                var email = credentials[0];
                var password = credentials[1];

                // Step 8: Query the database for the user by email.
                var user = await _context.Users.SingleOrDefaultAsync(u => u.Email == email);
                if (user == null || !PasswordHasher.VerifyPassword(user.PasswordHash, password))
                {
                    // User not found or password incorrect.
                    return AuthenticateResult.Fail("Invalid Username or Password");
                }

                // Step 9: Create claims that represent the user's identity and roles.
                var claims = new List<Claim>
                {
                    new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), // Unique user ID
                    new Claim(ClaimTypes.Name, user.Email) // Username or email
                };

                // Step 10: Split roles by comma and add multiple claims
                var roles = user.Role.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

                // Add a claim for each role
                foreach (var role in roles)
                {
                    claims.Add(new Claim(ClaimTypes.Role, role));
                }

                // Step 11: Create a ClaimsIdentity with the authentication scheme name.
                var claimsIdentity = new ClaimsIdentity(claims, Scheme.Name);

                // Step 12: Create ClaimsPrincipal that holds the ClaimsIdentity (and potentially multiple identities).
                var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

                // Step 13: Create an AuthenticationTicket which encapsulates the user's identity (ClaimsPrincipal) and scheme info.
                // AuthenticationTicket is the object used by ASP.NET Core to store and
                // track the authenticated user’s ClaimsPrincipal during an authentication session.
                var authenticationTicket = new AuthenticationTicket(claimsPrincipal, Scheme.Name);

                // Step 14: Return success result with the AuthenticationTicket indicating successful authentication.
                return AuthenticateResult.Success(authenticationTicket);
            }
            catch
            {
                // If any unexpected error occurs during authentication, fail with a generic error.
                return AuthenticateResult.Fail("Error occurred during authentication");
            }
        }
    }
}
Use Role-Based Authorization Attributes on Controllers or Actions

To restrict access by role, add the Roles parameter to the [Authorize] attribute wherever you want to do so. So, please modify the ProductController as follows.

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

namespace BasicAuthenticationDemo.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ProductController : ControllerBase
    {
        private readonly IProductService _productService;
        public ProductController(IProductService productService)
        {
            _productService = productService;
        }

        // GET: api/product
        // Public endpoint - No authentication required
        [AllowAnonymous]
        [HttpGet]
        public async Task<ActionResult<List<ProductResponseDTO>>> GetAll()
        {
            var products = await _productService.GetAllAsync();
            return Ok(products);
        }

        // GET: api/product/{id}
        // Any authenticated user (no role required)
        [Authorize(AuthenticationSchemes = "BasicAuthentication")]
        [HttpGet("{id:int}")]
        public async Task<ActionResult<ProductResponseDTO>> GetById(int id)
        {
            var product = await _productService.GetByIdAsync(id);
            if (product == null)
                return NotFound(new { Message = $"Product with Id = {id} not found." });

            return Ok(product);
        }

        // POST: api/product
        // Requires single role: Administrator
        [Authorize(AuthenticationSchemes = "BasicAuthentication", Roles = "Administrator")]
        [HttpPost]
        public async Task<ActionResult<ProductResponseDTO>> Create(CreateProductDTO dto)
        {
            if (!ModelState.IsValid)
                return BadRequest(ModelState);

            var createdProduct = await _productService.CreateAsync(dto);
            return Ok(createdProduct);
        }

        // PUT: api/product/{id}
        // Requires multiple roles (OR): Administrator or Manager
        [Authorize(AuthenticationSchemes = "BasicAuthentication", Roles = "Administrator,Manager")]
        [HttpPut("{id:int}")]
        public async Task<IActionResult> Update(int id, UpdateProductDTO dto)
        {
            if (id != dto.Id)
                return BadRequest(new { Message = "Id in URL and payload must match." });

            if (!ModelState.IsValid)
                return BadRequest(ModelState);

            var updated = await _productService.UpdateAsync(id, dto);
            if (!updated)
                return NotFound(new { Message = $"Product with Id = {id} not found." });

            return NoContent();
        }

        // DELETE: api/product/{id}
        // Requires multiple roles (AND): Both Administrator and Manager
        [Authorize(AuthenticationSchemes = "BasicAuthentication", Roles = "Administrator")]
        [Authorize(AuthenticationSchemes = "BasicAuthentication", Roles = "Manager")]
        [HttpDelete("{id:int}")]
        public async Task<IActionResult> Delete(int id)
        {
            var deleted = await _productService.DeleteAsync(id);
            if (!deleted)
                return NotFound(new { Message = $"Product with Id = {id} not found." });

            return NoContent();
        }
    }
}

No changes required in the Program class. Now, run the application and test the API endpoints with different usernames and passwords, assigning different roles, and it should work as expected.

When to Use Role-Based Basic Authentication?

Use Role-Based Basic Authentication when:

  • You need a simple way to secure APIs, especially internal, test, or legacy systems.
  • Users can be assigned fixed roles (like “Admin”, “Manager”, “User”).
  • There is no need for advanced authentication methods (such as OAuth, JWT, or Single Sign-On).
  • Security is essential, but not the highest concern (since Basic Auth sends credentials encoded, not encrypted, it should always be used over HTTPS).

Avoid using it for large public APIs or where high security is needed, as Basic Auth is not as strong as token-based mechanisms. In the next article, I will discuss implementing JWT Authentication in an 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 article on ASP.NET Core Web API Role-Based Basic Authentication.

Leave a Reply

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