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

“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

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

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

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

[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

https://github.com/umayangag/AuthDemo

References and Further Reading

  1. https://code-maze.com/identity-asp-net-core-project/
  2. https://code-maze.com/user-registration-aspnet-core-identity/
  3. https://code-maze.com/authentication-aspnet-core-identity/
  4. https://code-maze.com/blazor-webassembly-series/
  5. https://code-maze.com/blazor-webassembly-role-based-authorization/
  6. https://code-maze.com/blazor-webassembly-authentication-aspnetcore-identity/

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store