How to Build an authentication handler for a minimal API in ASP.NET Core
By FoxLearn 12/31/2024 2:01:03 AM 177
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.
- Content Negotiation in Web API
- How to fix 'InvalidOperationException: Scheme already exists: Bearer'
- How to fix System.InvalidOperationException: Scheme already exists: Identity.Application
- Add Thread ID to the Log File using Serilog
- Handling Exceptions in .NET Core API with Middleware
- InProcess Hosting in ASP.NET Core
- Limits on ThreadPool.SetMinThreads and SetMaxThreads
- Controlling DateTime Format in JSON Output with JsonSerializerOptions