JWT Authentication with ASP.NET Core 3.1 Identity for Web APIs
Intensive online research made me realize that most of the online materials available regarding this topic are inaccurate, outdated, incomplete or not comprehensible enough for a beginner. This article aims to provide an up-to-date step-by-step guide for a beginner to implement JWT Authentication for ASP.NET Web API using ASP.NET Core Identity.
Creating the Project
We are using the ASP.NET Core 3.1 web application project with no authentication template because we want to do it by ourselves, from scratch.
We will need to install the following packages to complete this tutorial. Listing down all the commands in one go. Use the Package Manager Console to execute the following commands.
Install-Package EntityFrameworkCore.SqlServerInstall-Package Microsoft.EntityFrameworkCore.ToolsInstall-Package Microsoft.EntityFrameworkCore.DesignInstall-Package Microsoft.AspNetCore.Identity.EntityFrameworkCoreInstall-Package AutoMapper.Extensions.Microsoft.DependencyInjectionInstall-Package AutoMapperInstall-Package Microsoft.AspNetCore.Authentication.JwtBearer
Configuring Database Context
Add the following configuration settings to your appsettings.json file
“ConnectionStrings”: {“DBContext”: “Server=(localdb)\\mssqllocaldb;Database=AuthDemoDB;Trusted_Connection=True;MultipleActiveResultSets=true”}
Create a new directory named Data in the project directory and add the following class.
public class DBContext : DbContext{public DBContext(DbContextOptions<DBContext> options): base(options){}protected override void OnModelCreating(ModelBuilder modelBuilder){base.OnModelCreating(modelBuilder);modelBuilder.ApplyConfiguration(new RoleConfiguration());modelBuilder.Entity<IdentityUserRole<string>>().HasKey(p => new { p.UserId, p.RoleId });}public DbSet<User> Users { get; set; }}
Create the RoleConfiguration Class in the Data directory. This class is used to seed Roles to the database once we create the database.
public class RoleConfiguration : IEntityTypeConfiguration<IdentityRole>{public void Configure(EntityTypeBuilder<IdentityRole> builder){builder.HasData(new IdentityRole{Name = “Visitor”,NormalizedName = “VISITOR”},new IdentityRole{Name = “Administrator”,NormalizedName = “ADMINISTRATOR”});}}
Add the following code under ConfigureServices function in Startup.cs
services.AddDbContext<DBContext>(opt => opt.UseSqlServer(Configuration.GetConnectionString(“DBContext”)));
Creating Identity Schema
Create a new directory named Models and add the following User class.
public class User : IdentityUser{public string FirstName { get; set; }public string LastName { get; set; }}
To add the Identity service to the project, add the following lines to Startup.cs
services.AddIdentity<User, IdentityRole>().AddEntityFrameworkStores<DBContext>();services.Configure<IdentityOptions>(options =>{options.Password.RequireDigit = false;options.Password.RequireNonAlphanumeric = false;options.Password.RequireUppercase = false;});
To migrate changes to the Database, execute the following command in the Package Manager Console.
Add-Migration CreatingIdentitySchemeUpdate-Database
User Registration
To register users, we will create a new class named UserRegistrationModel in the Models directory.
public class UserRegistrationModel{public string FirstName { get; set; }public string LastName { get; set; }[Required(ErrorMessage = “Email is required”)][EmailAddress]public string Email { get; set; }[Required(ErrorMessage = “Password is required”)][DataType(DataType.Password)]public string Password { get; set; }[DataType(DataType.Password)][Compare(“Password”, ErrorMessage = “The password and confirmation password do not match.”)]public string ConfirmPassword { get; set; }}
Register the Automapper service by adding the following lines of code to the ConfigureServices function in Startup.cs
// Auto Mapper Configurationsvar mappingConfig = new MapperConfiguration(mc =>{mc.AddProfile(new MappingProfile());});IMapper mapper = mappingConfig.CreateMapper();services.AddSingleton(mapper);
Create a controller class named AccountsController under the Controllers directory and add the following code.
[Route(“api/[controller]”)][ApiController]public class AccountsController : ControllerBase{private readonly IMapper _mapper;private readonly UserManager<User> _userManager;private readonly IConfigurationSection _jwtSettings;public AccountsController(IMapper mapper, UserManager<User> userManager, IConfiguration configuration){_mapper = mapper;_userManager = userManager;_jwtSettings = configuration.GetSection(“JwtSettings”);}[HttpPost(“Register”)]public async Task<ActionResult> Register(UserRegistrationModel userModel){var user = _mapper.Map<User>(userModel);var result = await _userManager.CreateAsync(user, userModel.Password);if (!result.Succeeded){return Ok(result.Errors);}await _userManager.AddToRoleAsync(user, “Visitor”);return StatusCode(201);}
}
Now you have successfully created user registration functionality, including Roles for users.
Authentication
To allow ASP.NET Core to provide authentication and authorization, add the following lines to the code after the line app.UseRouting(); under Configure function in Startup.cs
// Required for Authentication.app.UseAuthentication();app.UseAuthorization();
Now let's add the following JWT configuration into the appsettings.json
“JWTSettings”: {“securityKey”: “YourSecretKey”,“expiryInMinutes”: 600,“validIssuer”: “IssuerName”},
Next, you need to configure the JWT settings in the ConfigureServices function in Startup.cs, Add the following lines.
// JWT Configurationvar jwtSettings = Configuration.GetSection(“JwtSettings”);services.AddAuthentication(opt =>{opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;}).AddJwtBearer(options =>{options.TokenValidationParameters = new TokenValidationParameters{ValidateIssuer = false,ValidateAudience = false,ValidateLifetime = true,ValidateIssuerSigningKey = true,ValidIssuer = jwtSettings.GetSection(“validIssuer”).Value,ValidAudience = jwtSettings.GetSection(“validAudience”).Value,IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.GetSection(“securityKey”).Value))};});
Now let's create a model to allow the user to login and create corresponding controller actions.
To login users, we will create a new class named UserLoginModel in the Models directory.
public class UserLoginModel{[Required][EmailAddress]public string Email { get; set; }[Required][DataType(DataType.Password)]public string Password { get; set; }}
And next, add the following code to the AccountsController, which was already created in the previous section.
[HttpPost(“Login”)]public async Task<IActionResult> Login(UserLoginModel userModel){var user = await _userManager.FindByEmailAsync(userModel.Email);if (user != null && await _userManager.CheckPasswordAsync(user, userModel.Password)){var signingCredentials = GetSigningCredentials();var claims = GetClaims(user);var tokenOptions = GenerateTokenOptions(signingCredentials, await claims);var token = new JwtSecurityTokenHandler().WriteToken(tokenOptions);return Ok(token);}return Unauthorized(“Invalid Authentication”);}
Include the following helper functions in the AccountsController or Move them to a separate directory. Change the access modifiers accordingly.
private SigningCredentials GetSigningCredentials(){var key = Encoding.UTF8.GetBytes(_jwtSettings.GetSection(“securityKey”).Value);var secret = new SymmetricSecurityKey(key);return new SigningCredentials(secret, SecurityAlgorithms.HmacSha256);}private JwtSecurityToken GenerateTokenOptions(SigningCredentials signingCredentials, List<Claim> claims){var tokenOptions = new JwtSecurityToken(issuer: _jwtSettings.GetSection(“validIssuer”).Value,audience: _jwtSettings.GetSection(“validAudience”).Value,claims: claims,expires: DateTime.Now.AddMinutes(Convert.ToDouble(_jwtSettings.GetSection(“expiryInMinutes”).Value)),signingCredentials: signingCredentials);return tokenOptions;}private async Task<List<Claim>> GetClaims(User user){var claims = new List<Claim>{new Claim(ClaimTypes.Name, user.Email)};var roles = await _userManager.GetRolesAsync(user);foreach (var role in roles){claims.Add(new Claim(ClaimTypes.Role, role));}return claims;}
You can modify the return response value to include additional data by creating a new model class and returning the model upon successful login.
The returned JWT token can be stored in the client app and should be used in request headers as a bearer token for authentication.
Authorization
To secure your application endpoints, add the following Authorize attributes to your controllers or controller actions as shown below.
[Authorize] // allow access to any logged in user[ApiController][Route(“[controller]”)]public class WeatherForecastController : ControllerBase{...
Or else if you want to allow access only to certain user roles;
[Authorize(Roles = "Visitor")] // allow access only to visitor role[ApiController][Route(“[controller]”)]public class WeatherForecastController : ControllerBase{...
A Sample Draft Project
You can download a sample project at the following link.
https://github.com/umayangag/AuthDemo
References and Further Reading
- https://code-maze.com/asp-net-core-identity-series/
- https://code-maze.com/identity-asp-net-core-project/
- https://code-maze.com/user-registration-aspnet-core-identity/
- https://code-maze.com/authentication-aspnet-core-identity/
- https://code-maze.com/blazor-webassembly-series/
- https://code-maze.com/blazor-webassembly-role-based-authorization/
- https://code-maze.com/blazor-webassembly-authentication-aspnetcore-identity/