How to Build an authentication handler for a minimal API in ASP.NET Core

By FoxLearn 12/31/2024 2:01:03 AM   177
ASP.NET Core’s minimal API framework provides a lightweight approach to building APIs with minimal overhead, but security is still a priority.

In this guide, we’ll show how to implement basic password authentication for a minimal API in ASP.NET Core using a custom authentication handler that validates user credentials stored in a database, utilizing Entity Framework Core for data access.

Create the Minimal API

Start by creating a basic minimal API. Below is the code to set up a simple “Hello World!” endpoint:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello, World!");
app.Run();

Running this code displays “Hello World!” in your browser.

Enable Authentication

Authentication helps verify who the user is.

To enable authentication, you need to modify the Program.cs file to include AddAuthentication() as shown below:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication();
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();

This adds authentication capabilities to your API.

Install Entity Framework Core

For credential storage, we'll use Entity Framework Core with an in-memory database.

To install EF Core, open the NuGet Package Manager and install Microsoft.EntityFrameworkCore.InMemory or use the following command in the NuGet console:

PM> Install-Package Microsoft.EntityFrameworkCore.InMemory

Create the DbContext

Create a CustomDbContext class to represent the database connection. It will hold user data, including usernames and passwords:

public class CustomDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseInMemoryDatabase(databaseName: "MyAppDb");
    }
    
    public DbSet<User> Users { get; set; }
}

Define the User Class

Create a User class to hold user credentials:

public class User
{
    public string Username { get; set; }
    public string Password { get; set; }
}

In a real-world scenario, you would store credentials in a permanent database instead of an in-memory store.

Create the UserService

The UserService class will contain the logic to validate user credentials. Here’s how to implement the service:

public class UserService : IUserService
{
    private readonly CustomDbContext _dbContext;

    public UserService(CustomDbContext customDbContext)
    {
        _dbContext = customDbContext;
    }

    public async Task<User> AuthenticateAsync(string username, string password)
    {
        var user = await _dbContext.Users
            .SingleOrDefaultAsync(x => x.Username == username && x.Password == password);
        return user;
    }
}

The IUserService interface is as follows:

public interface IUserService
{
    Task<User> AuthenticateAsync(string username, string password);
}

Create the Custom Authentication Handler

Now, implement a custom authentication handler that verifies user credentials using basic authentication. Here’s an implementation of the HandleAuthenticateAsync method in the handler:

public class CustomAuthenticationHandler : AuthenticationHandler<CustomAuthenticationOptions>
{
    private readonly IUserService _userService;

    public CustomAuthenticationHandler(
        IOptionsMonitor<CustomAuthenticationOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock,
        IUserService userService)
        : base(options, logger, encoder, clock)
    {
        _userService = userService;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.ContainsKey("Authorization"))
        {
            return AuthenticateResult.Fail("Unauthorized");
        }

        var authHeader = Request.Headers["Authorization"].ToString();
        if (string.IsNullOrEmpty(authHeader))
        {
            return AuthenticateResult.NoResult();
        }

        User user;
        try
        {
            var authValue = AuthenticationHeaderValue.Parse(authHeader);
            var credentialBytes = Convert.FromBase64String(authValue.Parameter);
            var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':');
            var username = credentials[0];
            var password = credentials[1];

            user = await _userService.AuthenticateAsync(username, password);
            if (user == null)
                return AuthenticateResult.Fail("Invalid Username or Password");
        }
        catch
        {
            return AuthenticateResult.Fail("Invalid Authorization Header");
        }

        var claims = new List<Claim> { new Claim("Username", user.Username) };
        var claimsIdentity = new ClaimsIdentity(claims, Scheme.Name);
        var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

        return AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, Scheme.Name));
    }
}

The HandleAuthenticateAsync method checks if the authorization header is present in the HTTP request. If the header is missing, it returns an authentication failure. If the header exists, the method extracts and parses the username and password from the header. These credentials are then validated against the database. If invalid, authentication fails; if valid, a claims instance is created, and an authorization ticket is issued. This ticket is returned with a successful authentication result, allowing the pipeline to proceed.

Register the Authentication Handler

To use the custom authentication handler in your API, register it in Program.cs:

builder.Services.AddAuthentication("BasicAuthentication")
    .AddScheme<CustomAuthenticationOptions, CustomAuthenticationHandler>("BasicAuthentication", options => { });

In basic authentication, the client sends credentials in plaintext within the HTTP request. If the credentials are invalid, the server responds with a 401 Unauthorized status code. The AuthorizationHeaderName specifies the name of the HTTP header used to transmit these credentials.

And enable authentication and authorization middleware:

app.UseAuthentication();
app.UseAuthorization();

Create a Test Endpoint

Next, create a test endpoint that requires authentication. The endpoint will return a success message if authentication is successful:

app.MapGet("/test", [Authorize] async () =>
{
    return Results.Ok("Authenticated successfully");
});

This endpoint can only be accessed if the correct username and password are provided in the authorization header.

Test the Authentication

To test the authentication, use a tool like Postman. Set up an HTTP request to /test with the username and password in the Authorization header, encoded in base64. If successful, you’ll receive a 200 OK response. If the credentials are incorrect or missing, the response will be 401 Unauthorized.