JWT Authentication And Authorization In .NET 6.0 With Identity Framework

Introduction 

Microsoft released .NET 6.0 in November 2021. I have already written a couple of articles about JWT authentication on C# Corner. Since .NET 6.0 made some significant changes, I have decided to write one article about JWT authentication using .NET 6.0 version. We will be using the Microsoft Identity framework to store user and role information.  

Authentication is the process of validating user credentials, and authorization is the process of checking privileges for a user to access specific modules in an application. In this article, we will see how to protect an ASP.NET Core Web API application by implementing JWT authentication. We will also see how to use authorization in ASP.NET Core to supply access to various functionalities of the application. We will store user credentials in an SQL server database, and we will use Entity Framework and Identity framework for database operations. 

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA

In its compact form, JSON Web Tokens consist of three parts separated by dots (.), which are.

  • Header 
  • Payload 
  • Signature 

Therefore, a JWT typically looks like the following. 

xxxx.yyyy.zzzz 

Please refer to the link below for more details about JSON Web Tokens.  Introduction to JWT

Create ASP.NET Core Web API using Visual Studio 2022 

We need Visual Studio 2022 to create .NET 6.0 applications. We can choose the ASP.NET Core Web API template from Visual Studio 2022. 

CreateNewProject

We can give a suitable name for our project and choose the .NET 6.0 framework.  

AdditionalInfo

Our new project will be created in a few moments.  

We must install the 4 libraries below into the new project. 

  • Microsoft.EntityFrameworkCore.SqlServer 
  • Microsoft.EntityFrameworkCore.Tools 
  • Microsoft.AspNetCore.Identity.EntityFrameworkCore 
  • Microsoft.AspNetCore.Authentication.JwtBearer 

You can use the NuGet package manager to install the above packages. 

We can change the appsettings.json with values below. It has database connection details and other details for JWT authentication. 

appsettings.json 

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "ConnStr": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=JWTAuthDB;Integrated Security=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
  },
  "JWT": {
    "ValidAudience": "http://localhost:4200",
    "ValidIssuer": "http://localhost:5000",
    "Secret": "JWTAuthenticationHIGHsecuredPasswordVVVp1OH7Xzyr"
  }
}

We can create a new folder, “Auth” and create the “ApplicationDbContext” class under Auth folder and add below code. We will add all the classes related to authentication under the Auth folder. 

ApplicationDbContext.cs 

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace JWTAuthentication.NET6._0.Auth
{
    public class ApplicationDbContext : IdentityDbContext<IdentityUser>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        {
        }
        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
        }
    }
}

Create a static class “UserRoles” and add below values. 

UserRoles.cs 

namespace JWTAuthentication.NET6._0.Auth
{
    public static class UserRoles
    {
        public const string Admin = "Admin";
        public const string User = "User";
    }
}

We have added two constant values “Admin” and “User” as roles. You can add as many roles as you wish. 

Create class “RegisterModel” for new user registration. 

RegisterModel.cs 

using System.ComponentModel.DataAnnotations;

namespace JWTAuthentication.NET6._0.Auth
{
    public class RegisterModel
    {
        [Required(ErrorMessage = "User Name is required")]
        public string? Username { get; set; }

        [EmailAddress]
        [Required(ErrorMessage = "Email is required")]
        public string? Email { get; set; }

        [Required(ErrorMessage = "Password is required")]
        public string? Password { get; set; }
    }
}

Create class “LoginModel” for user login. 

LoginModel.cs 

using System.ComponentModel.DataAnnotations;

namespace JWTAuthentication.NET6._0.Auth
{
    public class LoginModel
    {
        [Required(ErrorMessage = "User Name is required")]
        public string? Username { get; set; }

        [Required(ErrorMessage = "Password is required")]
        public string? Password { get; set; }
    }
}

We can create a class “Response” for returning the response value after user registration and user login. It will also return error messages if the request fails. 

Response.cs 

namespace JWTAuthentication.NET6._0.Auth
{
    public class Response
    {
        public string? Status { get; set; }
        public string? Message { get; set; }
    }
}

We can create an API controller “AuthenticateController” inside the “Controllers” folder and add below code. 

AuthenticateController.cs 

using JWTAuthentication.NET6._0.Auth;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace JWTAuthentication.NET6._0.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthenticateController : ControllerBase
    {
        private readonly UserManager<IdentityUser> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;
        private readonly IConfiguration _configuration;

        public AuthenticateController(
            UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager,
            IConfiguration configuration)
        {
            _userManager = userManager;
            _roleManager = roleManager;
            _configuration = configuration;
        }

        [HttpPost]
        [Route("login")]
        public async Task<IActionResult> Login([FromBody] LoginModel model)
        {
            var user = await _userManager.FindByNameAsync(model.Username);
            if (user != null && await _userManager.CheckPasswordAsync(user, model.Password))
            {
                var userRoles = await _userManager.GetRolesAsync(user);

                var authClaims = new List<Claim>
                {
                    new Claim(ClaimTypes.Name, user.UserName),
                    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                };

                foreach (var userRole in userRoles)
                {
                    authClaims.Add(new Claim(ClaimTypes.Role, userRole));
                }

               var token = GetToken(authClaims);

                return Ok(new
                {
                    token = new JwtSecurityTokenHandler().WriteToken(token),
                    expiration = token.ValidTo
                });
            }
            return Unauthorized();
        }

        [HttpPost]
        [Route("register")]
        public async Task<IActionResult> Register([FromBody] RegisterModel model)
        {
            var userExists = await _userManager.FindByNameAsync(model.Username);
            if (userExists != null)
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User already exists!" });

            IdentityUser user = new()
            {
                Email = model.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
                UserName = model.Username
            };
            var result = await _userManager.CreateAsync(user, model.Password);
            if (!result.Succeeded)
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User creation failed! Please check user details and try again." });

            return Ok(new Response { Status = "Success", Message = "User created successfully!" });
        }

        [HttpPost]
        [Route("register-admin")]
        public async Task<IActionResult> RegisterAdmin([FromBody] RegisterModel model)
        {
            var userExists = await _userManager.FindByNameAsync(model.Username);
            if (userExists != null)
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User already exists!" });

            IdentityUser user = new()
            {
                Email = model.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
                UserName = model.Username
            };
            var result = await _userManager.CreateAsync(user, model.Password);
            if (!result.Succeeded)
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User creation failed! Please check user details and try again." });

            if (!await _roleManager.RoleExistsAsync(UserRoles.Admin))
                await _roleManager.CreateAsync(new IdentityRole(UserRoles.Admin));
            if (!await _roleManager.RoleExistsAsync(UserRoles.User))
                await _roleManager.CreateAsync(new IdentityRole(UserRoles.User));

            if (await _roleManager.RoleExistsAsync(UserRoles.Admin))
            {
                await _userManager.AddToRoleAsync(user, UserRoles.Admin);
            }
            if (await _roleManager.RoleExistsAsync(UserRoles.Admin))
            {
                await _userManager.AddToRoleAsync(user, UserRoles.User);
            }
            return Ok(new Response { Status = "Success", Message = "User created successfully!" });
        }

        private JwtSecurityToken GetToken(List<Claim> authClaims)
        {
            var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));

            var token = new JwtSecurityToken(
                issuer: _configuration["JWT:ValidIssuer"],
                audience: _configuration["JWT:ValidAudience"],
                expires: DateTime.Now.AddHours(3),
                claims: authClaims,
                signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
                );

            return token;
        }
    }
}

We have added three methods “login”, “register”, and “register-admin” inside the controller class. Register and register-admin are almost same, but the register-admin method will be used to create a user with admin role. In login method, we have returned a JWT token after successful login. 

In .NET 6.0, Microsoft removed the Startup class and only kept Program class. We must define all our dependency injection and other configurations inside the Program class. 

Program.cs 

using JWTAuthentication.NET6._0.Auth;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
ConfigurationManager configuration = builder.Configuration;

// Add services to the container.

// For Entity Framework
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(configuration.GetConnectionString("ConnStr")));

// For Identity
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

// Adding Authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})

// Adding Jwt Bearer
.AddJwtBearer(options =>
{
    options.SaveToken = true;
    options.RequireHttpsMetadata = false;
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidAudience = configuration["JWT:ValidAudience"],
        ValidIssuer = configuration["JWT:ValidIssuer"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Secret"]))
    };
});

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// Authentication & Authorization
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

We must create a database and tables needed before running the application. As we are using entity framework, we can use below database migration command with package manger console to create a migration script. 

add-migration Initial

PackageManagerConsole

Use the command below to create database and tables. 

update-database

If you check the database using SQL server object explorer, you can see that the tables below are created inside the database. 

Explorer

There are 7 tables created for User, Role and Claims during the database migration process. This is due to the Microsoft Identity framework.  

ASP.NET Core Identity is a membership system which allows you to add login functionality to your application. Users can create an account and login with a username and password, or they can use an external login provider such as Facebook, Google, Microsoft Account, Twitter and more. 

You can configure ASP.NET Core Identity to use a SQL Server database to store usernames, passwords, and profile data. Alternatively, you can use your own persistent store to store data in another other persistent storage, such as Azure Table Storage. 

We can add “Authorize” attribute inside the “WeatherForecast” controller. 

WeatherForecastController

We can run the application and try to access get method in weatherforecast controller from Postman tool.

GETPOSTMAN

We have received a 401 unauthorized error. Because, we have added Authorize attribute to entire controller. We must supply a valid token via request header to access this controller and methods inside the controller. 

We can create a new user using a register method in authenticate controller. 

POSTMANRegister

We have supplied the input data as raw JSON format.  

We can use the above user credentials to login and get a valid JWT token. 

PostmanLogin

We have received a token after successful login with the above credentials. 

We can decode the token and see the claims and other information using https://jwt.io site. 

JWTEncode

We can pass the above token value as a Bearer token inside the authorization tab and call get method of weatherforecast controller again. 

PostmanForecast

This time, we have successfully received the values from the controller. 

We can change the weatherforecast controller with role-based authorization. 

Roles

Now, only users with admin role can access this controller and methods. 

We can try to access the weatherforecast controller with same token again in Postman tool. 

WeatherPostman

We have received a 403 forbidden error instead of 401 now. Even though we are passing a valid token we don’t have sufficient privilege to access the controller. To access this controller, the user must have an admin role permission. Current user is a normal user and does not have any admin role permission. 

We can create a new user with an admin role permission. We already have a method “register-admin” in authenticate controller for the same purpose. 

POstmanAdmin

We can login with these new user credentials and get a new token. If you decode the token, you can see that the roles are added to the token. 

JWTPermission

We can use this token instead of the old token to access the weatherforecast controller. 

PostmanHeader

Now we have successfully fetched the data from weatherforecast controller.  

Conclusion 

In this post, we have seen how to create a JSON web token in .NET 6.0 ASP.NET Core Web API application and use this token for authentication and authorization. We have created two users, one without any role and one with admin role. We have applied authentication and authorization at controller level and saw the different behaviors with these two users.