How to implement basic password authentication for a minimal API in ASP.NET Core using a custom authentication handler that validates the user’s credentials against a database. Credit: Clem Onojeghuo ASP.NET Core offers a simplified hosting model, called minimal APIs, that allows us to build lightweight APIs with minimal dependencies. However, “minimal” doesn’t mean minimal security. Minimal APIs need authentication too. We’ve explored JWT authentication in an earlier post here. In this article we’ll examine how we can build a basic authentication handler for minimal APIs in ASP.NET Core. Below we’ll implement a basic authentication handler that will identify and authenticate the user. Because we will validate the user’s identity using credentials stored in a database, we will make use of Entity Framework Core. To use the code examples provided in this article, you should have Visual Studio 2022 installed in your system. If you don’t already have a copy, you can download Visual Studio 2022 here. Create an ASP.NET Core Web API project in Visual Studio 2022 To create an ASP.NET Core Web API project in Visual Studio 2022, follow the steps outlined below. Launch the Visual Studio 2022 IDE. Click on “Create new project.” In the “Create new project” window, select “ASP.NET Core Web API” from the list of templates displayed. Click Next. In the “Configure your new project” window, specify the name and location for the new project. Optionally check the “Place solution and project in the same directory” check box, depending on your preferences. Click Next. In the “Additional Information” window shown next, select “.NET 8.0 (Long Term Support)” as the framework version and uncheck the check box that says “Use controllers,” as we’ll be using minimal APIs in this project. Elsewhere in the “Additional Information” window, leave the “Authentication Type” set to “None” (the default) and make sure the check boxes “Enable Open API Support,” “Configure for HTTPS,” and “Enable Docker” remain unchecked. We won’t be using any of those features here. Click Create. We’ll use this ASP.NET Core Web API project to work with the code examples given in the sections below. Create a minimal API in ASP.NET Core You can replace the generated code with the following piece of code to create a basic minimal API. var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/", () => "Hello, World!"); app.Run(); When you execute the application, the text “Hello World!” will be displayed in your web browser. Enable authentication in a minimal API Authentication is the process of determining who the user is and validating the user’s identity. (Once the user is authenticated, we can determine the roles the user should have access to in the application. This process is known as authorization.) You can enable authentication in a minimal API in ASP.NET Core by using the AddAuthentication() method as shown in the code snippet given below. var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication(); var app = builder.Build(); app.MapGet("/", () => "Hello World!"); app.Run(); Install the EF Core NuGet package We’ll use the in-memory capabilities of Entity Framework Core to store our user credentials for authentication. To add the Microsoft.EntityFrameworkCore.InMemory package to your project, select the project in the Solution Explorer window, then right-click and select “Manage NuGet Packages.” In the NuGet Package Manager window, search for the Microsoft.EntityFrameworkCore.InMemory package and install it. Alternatively, you can install the package via the NuGet Package Manager console by entering the command shown below. PM> Install-Package Microsoft.EntityFrameworkCore.InMemory Create a new DbContext in EF Core The DbContext is an integral component of Entity Framework Core that represents a connection session with the database. Create a new class named CustomDbContext by extending the DbContext class of EF Core and enter the following code in there. public class CustomDbContext : DbContext { protected override void OnConfiguring (DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseInMemoryDatabase(databaseName: "IDGSampleDb"); } public DbSet<User> Users { get; set; } } Create a User class in ASP.NET Core Create a new class named User in a file called User.cs and write the following code in there. We’ll use this class to store our users and their passwords for authentication. public class User { public string Username { get; set; } public string Password { get; set; } } An instance of our User class here will store the credentials of a user in memory. Normally of course the credentials of a user would permanently reside in the database. Create a UserService class to validate user credentials Next let’s create a class called UserService that encapsulates the logic required to validate user credentials. The Authenticate method returns an instance of the User class if the credentials of the user passed to it as parameters are valid. The following code snippet shows the UserService class. public class UserService : IUserService { private readonly CustomDbContext _dbContext; public UserService(CustomDbContext customDbContext) { this._dbContext = customDbContext; } public async Task<User> Authenticate(string username, string password) { var user = await Task.Run(() => _dbContext.Users.SingleOrDefault (x => x.Username == username && x.Password == password)); return user; } } The IUserService interface is given below for your reference. public interface IUserService { Task<User> Authenticate(string username, string password); } Authentication schemes and authentication handlers In ASP.NET Core, an authentication scheme is used to specify how authentication should be performed for a request. An authentication scheme comprises a named set of options and behavior that are encapsulated by an authentication handler. An authentication handler in ASP.NET Core is a type that implements the behavior of an authentication scheme. An authentication handler extends the IAuthenticationHandler interface or the AuthenticationHandler type. An authentication handler should return success or failure depending on whether the authentication process has succeeded or failed. Create an authentication scheme for a minimal API Before you create a custom authentication handler, you should first create a custom AuthenticationSchemeOptions type as shown below. public class CustomAuthenticationSchemeOptions : AuthenticationSchemeOptions { public const string DefaultScheme = "BasicAuthentication"; public const string AuthorizationHeaderName = "Authorization"; } Next create a new class named User in a file called User.cs and enter the following code. public class User { public string Username { get; set; } public string Password { get; set; } } In the above code snippet, note how we have specified the authentication scheme. Authentication schemes are used to thwart any unauthorized access to sensitive information by verifying the identity of a user, device, or entity before access to a resource is granted. In this example, the default authentication scheme has been specified as BasicAuthentication. In basic authentication, a client passes credentials in plaintext while making an HTTP request to a server. The server will return a HTTP 401 Unauthorized status code, indicating that the authentication has failed, if the request is not legitimate. AuthorizationHeaderName indicates the name of the HTTP header that will be used to transmit the credentials as part of a HTTP request. Create an authentication handler for a minimal API In ASP.NET Core, the HandleAuthenticateAsync method is used in an authentication handler to encapsulate the code for authenticating a request. This method is a part of the AuthenticationHandler class. You should implement the HandleAuthenticateAsync method in your custom authentication handler including your custom code to authenticate a request. The following code listing shows the implementation of the HandleAuthenticateAsync overridden method. protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { if (!Request.Headers.ContainsKey(CustomAuthenticationOptions.AuthorizationHeaderName)) { return AuthenticateResult.Fail("Unauthorized"); } var authenticationHeaderValue = Request.Headers[CustomAuthenticationOptions.AuthorizationHeaderName]; if (string.IsNullOrEmpty(authenticationHeaderValue)) { return AuthenticateResult.NoResult(); } User user; try { var authenticationHeader = AuthenticationHeaderValue.Parse(authenticationHeaderValue); var credentialBytes = Convert.FromBase64String(authenticationHeader.Parameter); var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2); var username = credentials[0]; var password = credentials[1]; user = new User() { Username = username, Password = password }; user = await _userService.Authenticate(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, this.Scheme.Name)); } The HandleAuthenticateAsync method verifies whether the authorization header exists. If it doesn’t exist, the authentication handler returns an AuthenticateResult instance indicating a failure. If the authorization header is present, the handler retrieves the data present in the authorization header. This data is then parsed to retrieve the username and password of the user that was passed in the authorization header of the HTTP request. The retrieved credentials are then validated against the database. If the credentials are not valid, an AuthorizationResult instance is returned indicating failure. If the credentials are valid, a claims instance is created and then passed over an authorization ticket. This ticket is then returned using an instance of AuthenticateResult to allow the remaining modules of the pipeline to execute as usual. Register the authentication handler in ASP.NET Core To register the custom authentication handler with the request processing pipeline, you should include the following piece of code in the Program.cs file. builder.Services.AddAuthentication (CustomAuthenticationOptions.DefaultScheme) .AddScheme<CustomAuthenticationOptions, CustomAuthenticationHandler> (CustomAuthenticationOptions.DefaultScheme, options => { }); Lastly, you should include the following piece of code in the Program.cs file to take advantage of authentication and authorization. app.UseAuthentication(); app.UseAuthorization(); Complete authentication handler example in ASP.NET Core The complete source code of the custom authentication handler is given below for your reference. public class CustomAuthenticationHandler : AuthenticationHandler<CustomAuthenticationOptions> { public CustomAuthenticationHandler (IOptionsMonitor<CustomAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { if (!Request.Headers.ContainsKey(CustomAuthenticationOptions.AuthorizationHeaderName)) { return AuthenticateResult.Fail("Unauthorized"); } var authenticationHeaderValue = Request.Headers[CustomAuthenticationOptions.AuthorizationHeaderName]; if (string.IsNullOrEmpty(authenticationHeaderValue)) { return AuthenticateResult.NoResult(); } User user = null; try { var authenticationHeader = AuthenticationHeaderValue.Parse(authenticationHeaderValue); var credentialBytes = Convert.FromBase64String(authenticationHeader.Parameter); var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2); var username = credentials[0]; var password = credentials[1]; user = new User() { Username = username, Password = password }; if (user == null) return AuthenticateResult.Fail("Invalid credentials"); } catch { return AuthenticateResult.Fail("Authorization Header is invalid"); } 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, this.Scheme.Name)); } } Create an HTTP endpoint to test the authentication handler Consider the following code snippet that shows how you can create a HttpGet endpoint that requires authorization. This endpoint will be invoked only if you provide the correct credentials. app.MapGet("/test", [Authorize] async ([FromBody] User user) => { var userName = user.Username; var password = user.Password; return Results.Ok(); }); Finally, run both the application and the Postman tool to invoke the endpoint. Figure 1 shows how you can specify the username and password for the request in Postman. IDG Figure 1: Configuring basic authentication in Postman. You can now invoke the /test endpoint from Postman. Figure 2 shows the /test endpoint invoked from Postman. IDG Figure 2: Invoking the endpoint using Postman. If the authentication is successful, the API endpoint will return the HTTP 200 OK status code. If authentication fails, then the API endpoint will return the HTTP 401 Unauthorized status code. A minimalistic implementation Note that our minimalistic implementation here does not include any code to store the credentials of the user in the database. You should write your own implementation to accept credentials from the user and then store them in the underlying database. Also, for the sake of simplicity we’ve used an in-memory database here. You should of course use a persistent store for user credentials in a real application. Related content news Spin 3.0 supports polyglot development using Wasm components Fermyon’s open source framework for building server-side WebAssembly apps allows developers to compose apps from components created with different languages. By Paul Krill Nov 18, 2024 2 mins Microservices Serverless Computing Development Libraries and Frameworks how-to How to use DispatchProxy for AOP in .NET Core Take advantage of the DispatchProxy class in C# to implement aspect-oriented programming by creating proxies that dynamically intercept method calls. By Joydip Kanjilal Nov 14, 2024 7 mins Microsoft .NET C# Development Libraries and Frameworks news Microsoft’s .NET 9 arrives, with performance, cloud, and AI boosts Cloud-native apps, AI-enabled apps, ASP.NET Core, Aspire, Blazor, MAUI, C#, and F# all get boosts with the latest major rev of the .NET platform. By Paul Krill Nov 12, 2024 4 mins C# Generative AI Microsoft .NET feature Can Wasm replace containers? WebAssembly revolutionized browser apps, and promises to upend the server stack. How will it impact containers and Kubernetes? Six experts weigh in. By Bill Doerrfeld Nov 11, 2024 12 mins Containers Kubernetes Cloud Native Resources Videos