Dev

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


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.

  1. Launch the Visual Studio 2022 IDE.
  2. Click on “Create new project.”
  3. In the “Create new project” window, select “ASP.NET Core Web API” from the list of templates displayed.
  4. Click Next.
  5. 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.
  6. Click Next.
  7. 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.
  8. 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.
  9. 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("https://www.infoworld.com/", () => "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("https://www.infoworld.com/", () => "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<TOptions> 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.

auth handler minimal api 01 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.

auth handler minimal api 02 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.

Copyright © 2024 IDG Communications, Inc.



READ SOURCE

This website uses cookies. By continuing to use this site, you accept our use of cookies.