ASP.NET Core 8: Creating Custom Authentication Handler for Authenticating Users for the Minimal APIs

The ASP.NET Core is the superb technology for building modern web apps. One of the best features of ASP.ET Core is the ease of security implementation and its customization as per the application requirements. The Identity Object model of ASP.NET Core provides extensive customization to the developers so that they can implement the security logic as per the application's requirements where the Identity can be verified against the On-Premises and/or Cloud stores where the Users' credentials are kept. 

In this article we will see an implementation of the custom Authentication Handler to handle the authorization of the request to the Minimal APIs. The Figure 1 provides an idea of the implementation



Figure 1: The Authentication Handlers in ASP.NET Core Minimal APIs

To implement custom authentication handler, we will be using the following classes:

  • AuthenticationSchemeOptions:  This class is used to set the scheme for the authentication calls. The request to the API will be forwarded to the Authentication Handler based on this scheme so that the request can be authenticated.
  • AuthenticationHandler: This is the generic class. This class accepts an AuthenticationSchemeOptions as a generic type of parameter. This class contains the HandleAuthenticateAsync() method. This method is used to validate the request by reading the information like Header, Header Values and based on it the request is authenticated. This method can have the logic for validating the request and generating the claims. The HandleAuthenticateAsync() method returns AuthenticateResult object. This object contains the status of authentication e.g. Success, Fail, etc.
        

Step 1: Use the script shown in Listing 1 to create a database a table in SQL Server database to store application users.

Create Database AppSecurity;

Use AppSecurity;

Create Table Users (
  UserId UNIQUEIDENTIFIER PRIMARY KEY default NEWID(),
  FirstName Varchar(100) Not Null,
  LastName  Varchar(100) Not Null,
  Email varchar(100) Not Null,
  UserName varchar(100) Not Null Unique,
  [Password] varchar(20) Not Null 
);

Insert into Users (FirstName, LastName,Email,UserName, Password)
Values (
  'user', 'app', 'user.app@myemail.com','usrapp','P2ssword_'
);

Select * from Users;


Listing 1: SQL Script

Step 2: Open Visual Studio 2022 and create a new ASP.NET Core API Application and name it as Core_CustAuthHandler. Since we will be using SQL Server Database for storing users' information, we need to use Entity Framework Core.  In this project add packages for Entity Framework Core as shown in Figure 2.



Figure 2: Entity Framework Core Packages    

To use the Database-First model of Entity framework Core, run the command shown in the Listing 2 to generate Models.

dotnet ef dbcontext scaffold "Data Source=.\SqlExpress;Initial Catalog=AppSecurity;Integrated Security=SSPI;TrustServerCertificate=True" Microsoft.EntityFrameworkCore.SqlServer -o Models

Listing 2: Entity Framework Core Database-First Model Generation

Move the Connection string from the generated DbContext class of the Models folder to appsettings.json. In the ConnectionStrings section.  

Step 3: Let's create a repository service for handling CRUD operations on the User table using the User Model class. In the project, add a new folder named Services.  In this folder, add a new interface file named IUserService.cs. In this file, we will define an interface that contains the code for IUserService that contains methods for creating and authenticating the user. The code for the interface is shown in Listing 2.


using Core_CustAuthHandler.Models;

namespace Core_CustAuthHandler.Services
{
    public interface IUserService
    {
        Task<User> CreateUserAsync(User user);
        Task<bool> AuthenticateUserAsync(User user);

        Task<User> GetUserAsync(string userName);
    }
}

Listing 2: IUserService.cs

In the Services folder, add a new class file named UserService.cs. In this class file we will add code for UserService class. This class implements IUserService interface and all its methods. This class contains code for creating new user for the application and authenticating the code. The code for the UserService class is shown in Listing 3.


using Core_CustAuthHandler.Models;

namespace Core_CustAuthHandler.Services
{
    public class UserService : IUserService
    {

        AppSecurityContext _context;

        public UserService(AppSecurityContext context)
        {
            _context = context;
        }

        public async Task<bool> AuthenticateUserAsync(User user)
        {
            bool IsAutenticated = false;
            try
            {
                // 1. Check the UserName exist or not
                var findUser =  _context.Users.Where(u => u.UserName.Trim() == user.UserName.Trim()).FirstOrDefault();
                if (findUser == null)
                    throw new Exception($"User Name: {user.UserName} isnot found");
                // 2. Check for the Password Match
                if (String.Equals(findUser.Password.Trim(), user.Password.Trim()))
                {
                    IsAutenticated = true;
                }

                return await Task.FromResult(IsAutenticated);
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        public async Task<User> CreateUserAsync(User user)
        {
            try
            {
                var result = await _context.AddAsync(user);
                await _context.SaveChangesAsync();
                return result.Entity;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        public async Task<User> GetUserAsync(string userName)
        {
            var findUser = _context.Users.Where(u => u.UserName.Trim() == userName.Trim()).FirstOrDefault();
            return await Task.FromResult(findUser) ;
        }
    }
}

Listing 3: The UserService class code

Step 4: In the project, add a new folder named CustomAuthentication. In this folder we will add code for various classed to define logic for AuthenticationSchemeOptions and AuthenticationHandler. In this folder, add a new class file named AuthSchemeOptions.cs. In this class file we will add the code for class AuthSchemeOptions. This class will be derived from AuthenticationSchemeOptions base class. This class will be used by the AuthenticationHandler to verify the Authentication scheme and validate the user for Authentication. The code for the AuthSchemeOptions class is shown in Listing 4.


using Microsoft.AspNetCore.Authentication;

namespace Core_CustAuthHandler.CustomAutentication
{
    /// <summary>
    /// This will be the Authentication Scheme Options
    /// This will be used by the Authetication Handler 
    /// </summary>
    public class AuthSchemeOptions : AuthenticationSchemeOptions
    {
    }
}

Listing 4: The AuthSchemeOptions class code    

In the CustomAuthentication folder, add a class file named AuthHandler.cs. In this class file, we will add code for AuthHandler class. This class is derived from the AuthenticationHandler class. The AuthHandler class is injected with IUserService interface and the IOptionsMonitor interface. The IOptionsMonitor interface is used to notify the Auth Scheme options changes. The AuthHandler class implements the HandleAuthenticateAsync() method of the AuthenticationHandler abstract base class. This method contains the logic for reading the Request Header scheme value and then based on the scheme, read the credentials values. These credentials values are used to authenticate the user. The code for the AuthHandler is shown in Listing 5.


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

namespace Core_CustAuthHandler.CustomAutentication
{
    /// <summary>
    /// The Handler Class
    /// </summary>
    public class AuthHandler : AuthenticationHandler<AuthSchemeOptions>
    {

        private readonly IUserService userService;

        public AuthHandler(IUserService userService, IOptionsMonitor<AuthSchemeOptions> optionsMonitor,ILoggerFactory logger, UrlEncoder encoder) :base(optionsMonitor, logger, encoder)   
        {
            this.userService = userService;
        }

        /// <summary>
        /// Method to Handle the Request for Authentication
        /// </summary>
        /// <returns></returns>
        /// <exception cref="NotImplementedException"></exception>
        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {

            // 1. Check if the Autentication Header is present ot not

            if (!Request.Headers.ContainsKey("Authorization"))
                return AuthenticateResult.Fail("Authorization Header is not present in the request");

            // 2. Read the Header value
            var headerAuthValues = Request.Headers["Authorization"];
            // 3. Now parse these values and check for authentication
            try
            {
                //3.a. Parse 
                var authValues = AuthenticationHeaderValue.Parse(headerAuthValues);
                 
                var authCredentials = authValues.Parameter.Split(new[] { ':' }, 2);
                // 3.b. Read the UserName as Password
                User ? authUser = new User() 
                {
                  UserName= authCredentials[0],
                  Password= authCredentials[1]
                };

                bool isAuthenticate = await userService.AuthenticateUserAsync(authUser);

                if(!isAuthenticate)
                    return AuthenticateResult.Fail("User Autheitcation Failed");

                // 3.c. Lets Claim the Authentication Ticket
                // Read the UserId from the UserName
                var id = (await userService.GetUserAsync(authUser.UserName)).UserId;
                // Set the Claims
                var authClaims = new[] {
                        new Claim("Id", id.ToString()),
                        new Claim("Name", authUser.UserName),
                };
                var autIdentity = new ClaimsIdentity(authClaims, Scheme.Name);
                var authPrincipal = new ClaimsPrincipal(autIdentity);
                var authTicket = new AuthenticationTicket(authPrincipal, Scheme.Name);
                // Return the Authentication Ticket
                return AuthenticateResult.Success(authTicket);

            }
            catch (Exception ex)
            {
                return AuthenticateResult.Fail("Invalid Authorization header ");
            }
        }
    }
}

Listing 5: The AuthHandler class

Step 5: Modify the Program.cs by adding code for registering AppSecurityContext class, UserService. We will also add code for adding the Authentication and Authorization services. The code is shown in Listing 6.


builder.Services.AddDbContext<AppSecurityContext>(options => {
    options.UseSqlServer(builder.Configuration.GetConnectionString("AppSecurityDbContext"));
});


/*Register the UserService*/
builder.Services.AddScoped<IUserService, UserService>();

// Register the Custion Authentication Handler
builder.Services.AddAuthentication("Basic")
        .AddScheme<AuthSchemeOptions, AuthHandler>("Basic",null);
builder.Services.AddAuthorization();

Listing 6: The Program.cs to register services 

We are using the Authentication as Basic. We need to register the Authentication and Authorization middlewars as shown in Listing 7.

// Add Security Middlewares
app.UseAuthentication();
app.UseAuthorization();

Listing 7: The Middleware

Step 6: Let's add Endpoints for creating user and accessing separate Endpoint to make an authenticated class. The code for these endpoints is shown in Listing 8.

// Adding Endpoints
app.MapPost("/api/register", async (IUserService serv, User user) => { 
    await serv.CreateUserAsync(user);
    return Results.Ok   ($"The User: {user.UserName} is created successfully!!");
});
 

// The Endpoint with Authentication call
app.MapGet("/api/sample", [Authorize] () => {
    return Results.Ok("The call is authenticated");
});


Listing 8: The Endpoints

The Endpoint '/api/sample' uses the Authorize attribute. This means that the request must contain the Authentication Schema and the Credentials so that AuthHandler will be invoked to handle the authenticate request to make sure that the user is authenticated. 

Run the application and create a user as shown in Figure 3.



Figure 3: Register the User

Once the user is created, make the call the '/api/sample' endpoint by making the HTTP GET request by passing the Scheme and credentials as shown in Figure 4. Note I have used the Advanced REST Client.



Figure 4: The Authenticated GET Request

If the password is wrong, then the UnAuthorized error will be returned as shown in Figure 5.



Figure 5: The UnAuthorized response      

  

This is how we can implement the Custom Authentication Handler in ASP.NET Core. We can implement our own logic as per the application need.

The code for this article can be downloaded from this link.  

 



Popular posts from this blog

Uploading Excel File to ASP.NET Core 6 application to save data from Excel to SQL Server Database

ASP.NET Core 6: Downloading Files from the Server

ASP.NET Core 6: Using Entity Framework Core with Oracle Database with Code-First Approach