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!!