Creating a custom authentication handler with Asp.Net Core Minimal APIs

I am an enthusiastic software developer focusing on C#, .NET, Asp.NET, Azure and Angular.
Search for a command to run...

I am an enthusiastic software developer focusing on C#, .NET, Asp.NET, Azure and Angular.
No comments yet. Be the first to comment.
gRPC, a high-performance, open-source RPC (Remote Procedure Call) framework developed by Google, has gained immense popularity among developers for building efficient and robust APIs. This blog post will explore how to get started with gRPC services ...

GraphQL is a powerful query language and runtime that efficiently fetches and manipulates data from APIs. This blog post will explore how to get started with GraphQL in Asp.Net Core. What is GraphQL? GraphQL is a query language and runtime for APIs t...

Observability is a key concept in modern software development. It refers to the ability of a system to be monitored, measured, and understood in order to identify and address issues. In the context of ASP.NET Core, observability can help developers a...

Introduction to Blazor Blazor is a web framework from Microsoft that allows developers to build interactive web applications using C# and HTML. With Blazor, we can create web applications that run entirely in the browser without the need for plugins ...

In this previous article, we learned about caching in Asp.Net Core. We talked about In-Memory cache and Distributed cache using SQL Server cache. This article will discuss how to use Redis for caching in Asp.Net Core. What is Redis? Redis (REmote DIc...

In this article I created a custom middleware for securing an Api. For this one let's change the implementation using a custom authentication handler.
Authentication is the process that helps identify who are the users. The API authentication process validates the identity of the client attempting to make a connection by using an authentication protocol.
After the user is authenticated the authorization process starts. It is necessary user’s identity to identify the role of a user and act accordingly.
Authentication schemes provide a way to collect credentials and determine the identity of a user and is useful as a mechanism for referring to the authentication, challenge, and forbid behaviors of the associated handler.
During authentication, Web Agents communicate with the Policy Server to determine the proper credentials that must be retrieved from a user who is requesting resources.
Authentication schemes are registered inside of the ConfigureServices method. It happens by calling the AddAuthentication method on the IServiceCollection instance passed to the method. It can register multiple authentication schemes, whereas only one of them will be a default scheme
Authentication handlers can be created implementing the interface IAuthenticationHandler or deriving from AuthenticationHandler. Its primary responsibility is to authenticate users based on the authentication scheme's configuration and the incoming request context.
It is common to have a custom logic for authenticating a user inside of the handler.
It should return:
'no result' or 'failure' if authentication is unsuccessful.
'success' if authentication is successful
This implementation will be based in Basic Authentication, having a user model and a service to help in the authentication process.
The model is to represent the user. A new property was added to the Context allowing user's data to be saved and retrieved from the database through the UserService. The service will be used in the endpoint and in the AuthenticationHandler.
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
public class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
public DbSet<User> Users => Set<User>();
}
public interface IUserService
{
Task<User> Authenticate(string username, string password);
Task<IEnumerable<User>> GetAll();
Task Save(User user);
}
public class UserService : IUserService
{
private readonly TodoDb _dbContext;
public UserService(TodoDb todoDb)
{
this._dbContext = todoDb;
}
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;
}
public async Task<IEnumerable<User>> GetAll()
{
return await _dbContext.Users.ToListAsync();
}
public async Task Save(User user)
{
_dbContext.Users.Add(user);
await _dbContext.SaveChangesAsync();
}
}
Creating a custom authentication handler requires an AuthenticationSchemeOptions implementation. So this custom authentication scheme is to specify the scheme that will be used, for now it just derives from AuthenticationSchemeOptions.
public class CustomBasicAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
}
The validations will be inside of the method HandleAuthenticateAsync. The first validation is to check if it contains the Authorization header. After that, the header value will be parsed and the username and password will be obtained from it. The UserService class will be used to authenticate the user.
At the end the claims will be created and then passed over an AuthenticationTicket so the middleware can continue with the execution of the pipeline.
public class CustomBasicAuthenticationHandler : AuthenticationHandler<CustomBasicAuthenticationSchemeOptions>
{
private readonly IUserService _userService;
public CustomBasicAuthenticationHandler(IUserService userService,
IOptionsMonitor<CustomBasicAuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
_userService = userService;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey(Constants.AuthorizationHeaderName))
{
return AuthenticateResult.Fail("Header Not Found.");
}
var headerValue = Request.Headers[Constants.AuthorizationHeaderName];
User? user = null;
try
{
var authHeader = AuthenticationHeaderValue.Parse(headerValue);
var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
var username = credentials[0];
var password = credentials[1];
user = await _userService.Authenticate(username, password);
}
catch
{
return AuthenticateResult.Fail("Invalid Authorization Header");
}
if (user == null)
return AuthenticateResult.Fail("Invalid Username or Password");
var claims = new[] {
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
The following code registers the custom authentication handler and the custom authentication scheme.
builder.Services.AddAuthentication("BasicAuthentication")
.AddScheme<CustomBasicAuthenticationSchemeOptions, CustomBasicAuthenticationHandler>("BasicAuthentication",null);
builder.Services.AddAuthorization();
And also needs to be added before app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
It was added a new endpoint to add new users.
app.MapPost("/user", async (IUserService userService, User user) =>
{
await userService.Save(user);
return Results.Created($"/users/{user.Id}", user);
});
To add authorize requirements to the endpoints is necessary to decorate them using the [Authorize] attribute.
app.MapPost("/todoitems", [Authorize] async (IValidator<Todo> validator, Todo todo, TodoDb db) =>
{
ValidationResult validationResult = await validator.ValidateAsync(todo);
if (!validationResult.IsValid)
{
return Results.ValidationProblem(validationResult.ToDictionary());
}
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
The following images are showing the user being created, the basic authentication being added to the endpoint and the todoitem being created successfully.



This is just a simple example of how to customize the authentication handler and authentication scheme.
There are things that could be improved such as: username and password being encrypted and using a better solution for authenticating the user.
You can find the full code on my GitHub.