Integrate ASP.NET Core Identity with JWT and minimal APIs

In the previous part of  this multipart article series we implemented JWT authentication in CRUD minimal APIs. Recollect that we are using a hard-coded user name and password while issuing the token in the getToken() handler function. Wouldn't it be nice if we integrate ASP.NET Core Identity into  app so that membership features such as account creation and user validations are taken care of easily? That's the agenda for this part of the article.

Open the same project that we have developed earlier in Visual Studio. Since we want to integrate ASP.NET Core Identity we need to install the following NuGet packages.

The Microsoft.AspNetCore.Identity.EntityFrameworkCore package contains everything required to add Identity support in our application. Notice that we have also added Microsoft.EntityFrameworkCore.Design because we want to generate EF Core migrations for creating Identity tables in the Northwind database. If you already have these tables in the Northwind database, you can skip adding the Microsoft.EntityFrameworkCore.Design.

Now go to the Program.cs file and add the AppIdentityDbContext class at the end.

public class AppIdentityDbContext : 
IdentityDbContext<IdentityUser, IdentityRole, string>
{
    public AppIdentityDbContext
       (DbContextOptions<AppIdentityDbContext> options)
        : base(options)
    {
    }
}

Notice the code marked in bold letters. The AppIdentityDbContext class inherits from IdentityDbContext. We also specify the types for TUser, TRole, and TKey. The Identityuser and IdentityRole types are readily available in the ASP.NET Core Identity framework. We could have also derived from them and specify the subclasses there. But for our example this much is sufficient.

Register the AppIdentityDbContext with the DI container by adding this code just below the earlier AddDbContext() call.

var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.
GetConnectionString("AppDb");
builder.Services.AddDbContext<AppDbContext>(o 
=> o.UseSqlServer(connectionString));
builder.Services.AddDbContext<AppIdentityDbContext>(o 
=> o.UseSqlServer(connectionString));

Also, add ASP.NET Core Identity types to DI by writing the following code just before the AddAuthentication() call.

builder.Services.AddIdentity<IdentityUser,IdentityRole>()
                .AddEntityFrameworkStores<AppIdentityDbContext>()
                .AddDefaultTokenProviders();

builder.Services.AddAuthentication(...);
...
...

We used Addidentity() and AddEntityFrameworkStores() to accomplish the task.

It's a good idea to save your work and build the project before you continue with the further steps.

Next, open NuGet Package Manager Console (Tools > NuGet Package manager > Package Manager Console) and issue this command:

> dotnet tool list --global

If dotnet-ef tool is installed on your machine you will see it listed like this:

If the dotnet-ef tool isn't installed on your machine you will need to install it using the following command:

> dotnet tool install --global dotnet-ef

After successfully installing the tool confirm it using the list command discussed earlier.

Then issue the following command in the Package Manager Console.

> dotnet ef migrations add IdentityMigration 
         --context AppIdentityDbContext 
         --project MinimalAPI

We specify the migration name to be IdentityMigration. The --context switch specifies the DbContext to be used for the migrations. Since we have two DbContext classes we explicitly specify AppIdentityDbContext using this switch. The --project switch specifies the project name. My project name is MinimalAPI. Make sure to specify whatever project name you have given.

The following figure shows a successful run of this command:

You will see Migrations folder getting created in the project as shown below:

You can now update the Northwind database with these migrations. To do so, issue the following command in the Package Manager Console.

> dotnet ef database update 
         --context AppIdentityDbContext
         --project MinimalAPI

The "update" command will update the underlying database and a set of tables will be created for you. You can see these "AspNet" tables in the following figure.

Now that we have wired ASP.NET Core Identity into the app, we will do two things. Firstly, we will create another minimal API for creating user accounts. Secondly, we will modify the getToken endpoint to use Identity instead of hard-coded user name and password.

Add the following MapPost() call that defines the createUser endpoint.

app.MapPost("/minimalapi/security/createUser", 
[AllowAnonymous] 
async(UserManager<IdentityUser> userMgr, User user) =>
{
    var identityUser = new IdentityUser() {
        UserName = user.UserName,
        Email = user.UserName + "@example.com"
    };

    var result = await userMgr.CreateAsync
(identityUser, user.Password);

    if(result.Succeeded)
    {
        return Results.Ok();
    }
    else
    {
        return Results.BadRequest();
    }
});

The endpoint URL is /minimalapi/security/createUser and its handler accepts two parameters - UserManager and User. The UserManager will be injected by DI whereas the User will be supplied by the client calling the API. Although not mandatory the handler is decorated with [AllowAnonymous] attribute to indicate that it doesn't require any authentication. Moreover, it is marked with async keyword.

Inside, we create a new IdentityUser object by specifying UserName and Email properties. We then attempt to create a user by calling CreateAsync() method of the UserManager. If the user creation succeeds we return HTTP status code 200 (Ok), otherwise we return HTTP status code 400 (Bad Request).

If successful, the user account information will be persisted in the Identity tables discussed earlier.

Now, modify the getToken endpoint as shown below:

app.MapPost("/minimalapi/security/getToken", 
[AllowAnonymous]
async (UserManager<IdentityUser> userMgr, User user) =>
{
    var identityUsr = await userMgr.FindByNameAsync(user.UserName);

    if (await userMgr.CheckPasswordAsync(identityUsr,user.Password))
    {
        var issuer = builder.Configuration["Jwt:Issuer"];
        var audience = builder.Configuration["Jwt:Audience"];
        var securityKey = new SymmetricSecurityKey
    (Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]));
        var credentials = new SigningCredentials(securityKey, 
SecurityAlgorithms.HmacSha256);
        var token = new JwtSecurityToken(issuer: issuer, 
            audience: audience, 
            signingCredentials: credentials);
        var tokenHandler = new JwtSecurityTokenHandler();
        var stringToken = tokenHandler.WriteToken(token);
        return Results.Ok(stringToken);
    }
    else
    {
        return Results.Unauthorized();
    }
});

Notice the code marked in bold letters. The getToken handler is now async and is decorated with [AllowAnonymous] attribute. We inject UserManager into the handler in addition to the User object.

Inside, we retrieve the IdentityUser whose UserName matches with the supplied value. This is done using FindByNameAsync() method of the UserManager. We then check whether the IdentityUser and Password match using the CheckPasswordAsync() method.

If all goes well, we create a JWT as discussed in the previous part of this article series and return it to the client.

Let's check the working on createUser and getToken APIs.

Run the application and launch the Swagger UI.

First, create a new user using the createUser API as shown below:

Specify some UserName and Password in the JSON format and hit on the Execute button to create the user.

Once the user is created you can invoke the getToken API to get a JWT for that user. The following figure shows a successful user validation followed by JWT creation.

Set this JWT in the authorization header using the following Swagger dialog (See previous part for more information about using this dialog).

Finally, execute the GET endpoint to retrieve the Employees as shown below:

In the next part of this series we will discuss how to use "Map" methods if your project uses Startup class based initialization. 

That's it for now! Keep coding!!


Bipin Joshi is an independent software consultant and trainer by profession specializing in Microsoft web development technologies. Having embraced the Yoga way of life he is also a yoga mentor, meditation teacher, and spiritual guide to his students. He is a prolific author and writes regularly about software development and yoga on his websites. He is programming, meditating, writing, and teaching for over 27 years. To know more about his private online courses on ASP.NET and meditation go here and here.

Posted On : 15 December 2021


Tags : ASP.NET ASP.NET Core MVC .NET Framework C# Visual Studio