Building Authentication Service using Microsoft ASP.NET Core Identity and .NET 8

Chamara Madushanka Dodandeniya
Aeturnum
Published in
7 min readJun 6, 2024

Microsoft Identity is a powerful feature that comes with the .NET framework and .NET Core packages. It allows developers to enable complete user login flows with ASP.NET web UI interfaces. However, the biggest disadvantage was the dependency on the ASP.NET web interfaces for the UIs and the difficulties in implementing SPA applications with custom login flows. With the introduction of .NET 8, Microsoft has addressed this issue by introducing ASP.NET Core Identity APIs, which provide a more robust and secure web API backend for SPAs. In this article, we will discuss what ASP.NET Core Identity is, the issues with the new Identity APIs, and how to integrate it step-by-step while customizing Identity APIs.

What is ASP.NET Core Identity?

In simple terms, ASP.NET Core Identity is a NuGet library provided by Microsoft, which enables a set of APIs that handle authentication, authorization, and identity management. These APIs can be used to secure backend endpoints using either cookie-based authentication or token-based authentication.

ASP.Net Core Identity provides the following key features

  1. User authentication and authorization
  2. User and role management
  3. Password hashing
  4. Two-Factor Authentication (2FA)
  5. Claims-Based Authorization

Limitations of ASP.NET Core Identity

Even though it provides powerful APIs, the biggest issue is customizing the endpoints. Hopefully, this will be fixed in an upcoming .NET release or later. Until then, this article will discuss how we can extend the existing endpoints provided by ASP.NET Core Identity.

Additionally, the access token provided by this library is not configured for JWT access tokens. Hence, if you are trying to use it for more complex and secure systems, you must consider using OpenID Connect or a similar approach combined with ASP.NET Core Identity features for authentication.

How to Integrate ASP.NET Core Identity Step by Step?

Prerequisites

Before starting the development, ensure you have installed the following required SDKs and libraries:

  1. NET 8 or higher installed
  2. EF Core installed globally (dotnet tool install — global dotnet-ef)
  3. IDE with .Net supported ( Visual Studio or Visual Studio Code)
  4. Familiar with C#

Project Setup

Let’s create a new .NET project by executing the following command in the terminal or Command Prompt. This will set up a project with Controllers template.

dotnet new webapi -controllers -n AuthService

Now, let’s open the project using Visual Studio or Visual Studio Code and install the required NuGet packages either using the package manager in Visual Studio or by using the terminal.

dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.AspNetCore.OpenApi

Next, let’s create the following directories inside the project solution root level, which we will use later:

Directories — Data, Dtos, Extensions, Models, Swagger

AuthService Project Directory structure

Identity Configuration Step by Step

Now, let’s create a User.cs file under the Models directory. We will use this model class to extend the IdentityUser class provided by ASP.NET Core Identity.

using Microsoft.AspNetCore.Identity;

namespace AuthService.Models;

public class User : IdentityUser
{

}

After that, let’s add an AppDBContext.cs class under the Data folder. We will use this class to set up the database context of the AuthService. The class extends the IdentityDbContext generic class, and we pass the newly created User data type to the IdentityDbContext class.

using AuthService.Models;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace AuthService.Data;

public class AppDBContext(DbContextOptions<AppDBContext> options) : IdentityDbContext<User>(options)
{
}

Now, let’s update the Program.cs file as follows:

using AuthService.Data;
using AuthService.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Add Authentication
builder.Services.AddAuthentication()
.AddBearerToken(IdentityConstants.BearerScheme);


// Add Authorization
builder.Services.AddAuthorization();

// Configure DBContext
builder.Services.AddDbContext<AppDBContext>(opt => opt.UseSqlite("DataSource=appdata.db"));

// Add IdentityCore
builder.Services
.AddIdentityCore<User>()
.AddEntityFrameworkStores<AppDBContext>()
.AddApiEndpoints();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

// Enable Identity APIs
app.MapIdentityApi<User>();

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

In the above section, you can see we have configured Authentication as a Bearer Token. Further, we have configured the Database context and used SQLite as the database. Lastly, we have enabled the default Identity APIs with app.MapIdentityApi<User>();.

Now, let’s perform the database migrations using Entity Framework Core by executing the following commands in the terminal. This will create the appdata.db database in the project's root directory with the initial identity tables.

dotnet ef migrations add InitialMigrations
dotnet ef database update
appdata.db

After the database migrations, you can run the app using the dotnet run command. Then, you can open the Swagger documentation in the browser by following this URL: http://localhost:{YOUR PORT NUMBER}/swagger/index.html.

Swagger Doc with default Identity APIs

How simple isn’t it?

Extend the “/register" Endpoint

If you expand the /register endpoint in the Swagger documentation, you can see it's only requesting the email and password for user registration. So, how can we extend this endpoint?

As I explained earlier, ASP.NET Core Identity does not provide a way to extend the default APIs. Therefore, we need to develop a custom extension method to extend the existing APIs.

Before that, let’s update the User.cs class by adding FirstName and LastName columns to the database.

using Microsoft.AspNetCore.Identity;

namespace AuthService.Models;

public class User : IdentityUser
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}

Now, let’s do the DB migration

dotnet ef migrations add UpdateUserTable
dotnet ef database update

After that, let’s create the CreateUserDto.cs class in the Dtos directory. This DTO class will be used for the user registration endpoint.

namespace AuthService.Dtos.User;

public record CreateUserDto(
string Email,
string Password,
string FirstName,
string LastName
);

Now, let’s create CustomIdentityApiEndpointExtensions.cs class inside the Extensions directory.

...

namespace AuthService.Extensions;

public static class IdentityApiEndpointRouteBuilderExtensions
{
private static readonly EmailAddressAttribute _emailAddressAttribute = new();


public static IEndpointConventionBuilder CustomMapIdentityApi<TUser>(this IEndpointRouteBuilder endpoints)
where TUser : User, new()
{
...

routeGroup.MapPost("/register", async Task<Results<Ok, ValidationProblem>>
([FromBody] CreateUserDto registration, HttpContext context, [FromServices] IServiceProvider sp) =>
{
var userManager = sp.GetRequiredService<UserManager<TUser>>();

if (!userManager.SupportsUserEmail)
{
throw new NotSupportedException($"{nameof(CustomMapIdentityApi)} requires a user store with email support.");
}

var userStore = sp.GetRequiredService<IUserStore<TUser>>();
var emailStore = (IUserEmailStore<TUser>)userStore;
var email = registration.Email;

if (string.IsNullOrEmpty(email) || !_emailAddressAttribute.IsValid(email))
{
return CreateValidationProblem(IdentityResult.Failed(userManager.ErrorDescriber.InvalidEmail(email)));
}

// Add customization to the Endpoint
var user = new TUser{
FirstName = registration.FirstName,
LastName = registration.LastName
};

await userStore.SetUserNameAsync(user, email, CancellationToken.None);
await emailStore.SetEmailAsync(user, email, CancellationToken.None);
var result = await userManager.CreateAsync(user, registration.Password);

if (!result.Succeeded)
{
return CreateValidationProblem(result);
}

await SendConfirmationEmailAsync(user, userManager, context, email);
return TypedResults.Ok();
});

...

}

...
}

You can find the full CustomIdentityApiEndpointExtensions.cs in the GitHub repository.

Now, you need to update the Program.cs file to use the custom identity endpoint extension we have created.

...

// Enable Identity APIs
app.CustomMapIdentityApi<User>();

...

Add “/logout” Endpoint

In the default Identity APIs, you cannot find any /logout endpoint. Let's add a logout endpoint to the extensions method. Note that currently, this only supports Cookie authentication.

First, let’s update the Program.cs file to support cookie authentication.

...
// Add Authentication
builder.Services.AddAuthentication()
.AddCookie(IdentityConstants.ApplicationScheme);
...

Next, let’s define the /logout endpoint in the extensions method.

...
routeGroup.MapPost("/logout", async Task<IResult> ([FromServices] IServiceProvider sp, [FromBody] object empty) => {
var signInManager = sp.GetRequiredService<SignInManager<TUser>>();

if(empty != null){
await signInManager.SignOutAsync();
return Results.Ok();
}
return Results.Unauthorized();
})
.WithOpenApi()
.RequireAuthorization();
...

By adding.RequireAuthorization(), the endpoint requires authorization before it can be called.

Now, let’s run the application, and you can see the newly added endpoint in the Swagger documentation.

Securing Endpoints

With .NET, it is easy to authorize endpoints by adding an [Authorize] attribute to the endpoint controller. Let's add the [Authorize] attribute to the WeatherForecast controller.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace AuthService.Controllers;

[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

private readonly ILogger<WeatherForecastController> _logger;

public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}

[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}

Now, let’s run the application and try to execute the WeatherForecast endpoint without authorization and with authorization.

Without authorization
With authorization

You can find the full source code from GitHub repository

Conclusion

Identity is a powerful feature built into ASP.NET Core. It makes managing authentication and authorization easy and secure. With .NET 8, Identity is even better and more flexible, giving developers many tools to handle security needs in their apps. This version has improved the Identity API, making it simpler and more intuitive to use. Developers can now use Identity with less code, making the implementation and maintenance easier.

In this article, we have discussed how to customize the existing Identity endpoints with the help of extension methods and secure endpoints with the authorization attribute provided by .NET Identity. While Identity is great for building authentication systems in SPAs, be aware of the risks when using its native token system. Using Identity can save a lot of time in analysis and development, so consider using it if it suits your needs.

Happy coding!

--

--