Hello everyone, in this blog post, I’m going to show you how to use the ASP.NET Core Identity framework with JWT tokens. We’ll create authentication and authorization support for our ASP.NET Core web API.
First, let’s briefly explain authentication and authorization.
Authentication is about verifying who we are, while authorization determines what we are allowed to do.
For example, imagine we have an application with a registered free account, and this account tries to access a paid service. This is a common case of authenticated but unauthorized access. The system knows who we are, but we are not allowed to access that service.
Now, after this quick explanation, I’ll show you the startup project we’ll be working on.
I’ve created a basic web API and added a few placeholder classes to keep this demonstration simple. We’ll review these classes throughout the video, so you’ll understand everything I’m discussing
Since we need some NuGet packages, I’ve already installed them to save time. I’ll now show you all of the packages installed in this project.
Now let’s start writing some code. First, we need to register a few services.
We’ll begin by adding an authorization policy, followed by the authentication services. We’ll return to this part of the code later to extend it further.
To save our data, we need a connection to the database. We’ll achieve this by registering our DbContext
. Since we decided to use SQL Server as our database provider, we specify that in the options configuration.
builder.Services.AddAuthorization();
builder.Services.AddAuthentication();
builder.Services.AddDbContext<WeatherStationDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});
Here, we are adding the ASP.NET Core Identity services for the User
entity, which we’ll customize for our needs. We’re also adding role management with the default IdentityRole
type, which can also be customized if needed. Finally, we configure Identity to use Entity Framework Core for storing identity information in our WeatherStationDbContext
.
builder.Services.AddIdentityCore<User>().AddRoles<IdentityRole>()
.AddEntityFrameworkStores<WeatherStationDbContext>();
To integrate our User
class and WeatherStationDbContext
with Identity, we need to configure them properly.
Our User
class should inherit from the IdentityUser
class, allowing us to customize it according to our requirements. We will add two additional properties.
public class User : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
The WeatherStationDbContext
class extends IdentityDbContext<User>
, specifying the User
entity for integration with ASP.NET Core Identity. Here, we will define roles that will be seeded into the database.
In the OnModelCreating
method, which is overridden from IdentityDbContext
We then define two roles, ‘Admin’ and ‘User’, and use builder.Entity<IdentityRole>().HasData(roles);
to seed these roles into the database during migration or application startup.
This setup ensures that our application is ready to manage user authentication and authorization with predefined roles
public class WeatherStationDbContext: IdentityDbContext<User>
{
public WeatherStationDbContext(DbContextOptions<WeatherStationDbContext> options): base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
var roles = new List<IdentityRole>()
{
new IdentityRole()
{
Name = "Admin",
NormalizedName = "ADMIN"
},
new IdentityRole()
{
Name = "User",
NormalizedName = "USER"
}
};
builder.Entity<IdentityRole>().HasData(roles);
}
}
Before running our initial migration I will paste the connection string into our configuration.
"ConnectionStrings": {
"DefaultConnection": "Server=localhost\\\\SQLEXPRESS;Database=WeatherStation;User Id=sa;Password=Blue.12.;MultipleActiveResultSets=True;TrustServerCertificate=True"
}
Now we can execute our first migration.
Within our Migrations folder, we’ll find our initial migration. Opening it reveals various tables related to users (roles, claims), all of which were automatically generated based on our IdentityDbContext
configuration.
After opening SQL Management Studio, we can view all the tables that have been created, as well as the two custom roles we added during seeding.
We are good to go now to start with implementation of our endpoints. First we will create our registration endpoint.
[HttpPost("register")]
public async Task<IActionResult> Register(RegisterUserViewModel registerUserViewModel)
{
var userAlreadyExists = await _userManager.FindByEmailAsync(registerUserViewModel.Email);
if (userAlreadyExists is not null)
return BadRequest("Email is already in use");
var user = new User()
{
FirstName = registerUserViewModel.FirstName,
LastName = registerUserViewModel.LastName,
Email = registerUserViewModel.Email,
UserName = registerUserViewModel.Email
};
await _userManager.CreateAsync(user, registerUserViewModel.Password);
await _userManager.AddToRoleAsync(user, "USER");
return Ok();
}
Before moving on, it’s worth noting that we’re using the user’s email address for both the Email and UserName fields. We did that on purpose because those two fields in the ASP.NET Core Identity system are used interchangeably by default
Once we finish our registration endpoint, we can create a new user. First, we’ll run our application. Next, we’ll open Postman and send a request to our register endpoint. If the request is successful, we should see a confirmation. By checking the database, we can verify that our newly created user has been added.
To allow our new user to log in, we need to create a login endpoint. Before doing that, we must be able to generate a JWT for our user. To accomplish this, we will create a JWT handler.
The JwtHandler
class is responsible for generating JWTs for authenticated users. It utilizes configuration settings and ASP.NET Core Identity to include user-specific claims and securely sign the tokens. The key methods are:
GetTokenAsync(User user)
: Creates and returns a JWT for the specified user.GetClaims(User user)
: Generates claims for the user, including roles.GetSigningCredentials()
: Generates the credentials needed to sign the JWT.
This class ensures that the generated tokens are correctly configured and securely signed, providing robust authentication for your application.
public class JwtHandler
{
private readonly IConfiguration _configuration;
private readonly UserManager<User> _userManager;
public JwtHandler(IConfiguration configuration, UserManager<User> userManager)
{
_configuration = configuration;
_userManager = userManager;
}
public async Task<JwtSecurityToken> GetTokenAsync(User user)
{
return new JwtSecurityToken(
issuer: _configuration["JwtSettings:Issuer"],
audience: _configuration["JwtSettings:Audience"],
expires: DateTime.Now.AddMinutes(Convert.ToDouble(_configuration["JwtSettings:ExpiresInMinutes"])),
claims: await GetClaims(user),
signingCredentials: GetSigningCredentials()
);
}
private async Task<IEnumerable<Claim>> GetClaims(User user)
{
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, user.Email)
};
foreach (var role in await _userManager.GetRolesAsync(user))
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
return claims;
}
private SigningCredentials GetSigningCredentials()
{
var key = Encoding.UTF8.GetBytes(_configuration["JwtSettings:SecurityKey"]!);
var secret = new SymmetricSecurityKey(key);
return new SigningCredentials(secret, SecurityAlgorithms.HmacSha256);
}
}
When sending requests to our API, we need to include the JWT bearer token generated by the login endpoint. To validate this token, we’ll update the Program.cs
file and extend our AddAuthentication
service registration.
In this step, we’ll define the authentication scheme and enable JWT bearer authentication by specifying the validation parameters.
The following code will register the JwtBearerMiddleware
, which will extract the JWT from the Authorization header and validate it using the configuration settings defined in the appsettings.json
file.
builder.Services.AddAuthorization();
builder.Services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters()
{
RequireExpirationTime = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidAudience = builder.Configuration["JwtSettings:Audience"],
ValidIssuer = builder.Configuration["JwtSettings:Issuer"],
IssuerSigningKey =
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JwtSettings:SecurityKey"]!))
};
});
It’s worth noting that, since we’re now using the authentication services, we also need to add AuthenticationMiddleware to the request pipeline in the Program.cs file. We can do that just before AuthorizationMiddleware, in the following way.
app.UseAuthentication(); -> New Line
app.UseAuthorization();
Now we need to add those values to our configuration file.
"JwtSettings": {
"SecurityKey": "1234567890-SecurityKey-1234567890",
"Issuer": "MyIssuer",
"Audience": "<https://localhost:4200>",
"ExpirationTimeInMinutes": 30
}
Explanation of Each Setting
- SecurityKey:
"1234567890-SecurityKey-1234567890"
- This is a secret key used to sign the JWT. It ensures the integrity and authenticity of the token.
- It should be a long and complex string to enhance security. In production, you should use a stronger, more secure key.
- Issuer:
"MyIssuer"
- This represents the entity that issues the JWT. It is a string identifier for the token issuer.
- Typically, this is the name of your application or authentication server.
- Audience:
"<https://localhost:4200>"
- This specifies the intended recipient(s) of the JWT. It ensures that the token is only accepted by the correct audience.
- In this case, it is set to
https://localhost:4200
, which could be the URL of your frontend application.
- ExpirationTimeInMinutes:
30
- This defines how long the JWT is valid after being issued.
- The value is in minutes. Here, the token will expire 30 minutes after it is issued.
The final step is to implement our Login
endpoint. First, we need to inject the JwtHandler
service into our controller. Then, we’ll check if the user exists and verify their password. If the credentials are valid, we’ll generate a token using the JwtHandler
.
The generated token is then converted to a string format that can be returned to the client.
[HttpPost("login")]
public async Task<IActionResult> Login(LoginViewModel loginViewModel)
{
var user = await _userManager.FindByEmailAsync(loginViewModel.Email);
if (user is null || !await _userManager.CheckPasswordAsync(user, loginViewModel.Password))
return Unauthorized("Invalid credentials");
//Get JWT
var token = await _jwtHandler.GetTokenAsync(user: user);
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
return Ok(new
{
Success = true,
Message = "Login successful",
Token = jwt
});
}
To test if everything is working, we will first log in with our previously created user. As we can see, the login is successful. Now we can use this JWT token to call other endpoints. For demonstration purposes, we currently have only one endpoint, to which we will add an Authorization attribute specifying the role that can access it.
After sending a request with the JWT token, we should see a successful response.