Implementing JWT Authentication and Authorization using Identity Framework in .NET 6.0

Implementing JWT Authentication and Authorization using Identity Framework in .NET 6.0

A simple process to introduce uploading and downloading multiple files using ASP.NET Core 6.0 Web API.

Last updated 7/25/2022 8:43 PM
Sarathlal Saseendran
13 min read
Category
.NET
Tags
.NET C# ASP.NET Core Web API Authentication

Original author: Sarathlal Saseendran

Original link: https://www.c-sharpcorner.com/article/jwt-authentication-and-authorization-in-net-6-0-with-identity-framework/

Translation: DesertEndWolf (with Google Translate assistance)

Introduction

Microsoft released .NET 6.0 in November 2021. I have written several articles about JWT authentication on C# Corner. Since .NET 6.0 introduced some significant changes, I decided to write an article about implementing JWT authentication using the .NET 6.0 version. We will use Microsoft's Identity framework to store user and role information.

Authentication is the process of verifying user credentials, while Authorization is the process of checking a user's permission to access specific modules in an application. In this article, we will learn how to secure an ASP.NET Core Web API application by implementing JWT authentication. We will also learn how to use authorization in ASP.NET Core to provide access to various application features. We will store user credentials in a SQL Server database (note: you can use other relational databases like MySQL, PostgreSQL, etc.), and we will use the EF Core 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 to securely transmit 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 this:

xxxx.yyyy.zzzz  

For more details about JSON Web Tokens, see the link below:

https://jwt.io/introduction/

Creating an ASP.NET Core Web API with Visual Studio 2022

We need Visual Studio 2022 to create a .NET 6.0 application. We can select the ASP.NET Core Web API template from Visual Studio 2022.

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

Our new project will be created shortly.

We need to install the following 4 libraries 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 these packages.

We can modify appsettings.json with the following values. It contains the 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 the Auth folder with the following code. We will add all authentication-related classes under the Auth folder.

ApplicationDbContext.cs

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

namespace IdentityDemo.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 the following values.

UserRoles.cs

namespace IdentityDemo.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 needed.

Create the class RegisterModel, used when registering a new user.

RegisterModel.cs

using System.ComponentModel.DataAnnotations;  

namespace IdentityDemo.Auth  
{  
    public class RegisterModel  
    {  
        [Required(ErrorMessage = "Username 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 the class LoginModel for user login.

LoginModel.cs

using System.ComponentModel.DataAnnotations;  

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

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

We can create a Response class to return response values after user registration and login. It will also return error messages if the request fails.

Response.cs

namespace IdentityDemo.Auth  
{  
    public class Response  
    {  
        public string? Status { get; set; }  
        public string? Message { get; set; }  
    }  
}  

We can create an API controller AuthenticateController in the Controllers folder and add the following code.

AuthenticateController.cs

using IdentityDemo.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 IdentityDemo.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 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 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.User))  
            {  
                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 in the controller class: login, register, and register-admin. register and register-admin are almost identical, but the register-admin method will be used to create users with the Admin role. In the login method, we return a JWT token after successful login.

In .NET 6.0, Microsoft removed the Startup class (note: you can still use it if you prefer) and kept only the Program class. We must define all dependency injections and other configurations in the Program class.

Program.cs

using IdentityDemo.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 the required database and tables before running the application. Since we are using Entity Framework (EF Core), we can use the following database migration command and the Package Manager Console to create a migration script:

add-migration Initial

Use the following command to create the database and tables:

update-database

If you check the database using SQL Server Object Explorer, you can see the following database tables created inside:

During the database migration, 7 tables were created for User, Role, and Claims. This is for the Identity framework.

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

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 storage to store data in another persistence store, such as Azure Table Storage.

We can add the Authorize attribute to the WeatherForecast controller.

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

We received a 401 unauthorized error. Because we added the Authorize attribute to the entire controller. We must provide a valid token in the request header to access this controller and its methods.

We can use the register method in AuthenticateController to create a new user.

We provided the input data in raw JSON format.

We can log in using the above user credentials and get a valid JWT token.

After successfully logging in with the above credentials, we received a token.

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

We can pass the above token value as a Bearer token in the Authorization tab and call the get method of the WeatherForecastController again.

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

We can change the WeatherForecastController to use role-based authorization.

Now, only users with the Admin role can access this controller and its methods.

We can try to access the WeatherForecastController again in the Postman tool using the same token.

We now received a 403 forbidden error instead of 401. Even though we passed a valid token, we do not have sufficient permissions to access the controller. To access this controller, the user must have the Admin role permission. The current user is a normal user without any Admin role permission.

We can create a new user with the Admin role permission. We already have a method register-admin in AuthenticateController for this purpose.

We can log in with these new user credentials and get a new token. If you decode the token, you can see that the role has been added to the token.

We can use this token instead of the old one to access the WeatherForecastController.

Now we have successfully retrieved data from the WeatherForecastController.

Conclusion

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

Keep Exploring

Related Reading

More Articles
Same category / Same tag 1/19/2024

FluentValidation Validation Tutorial Based on .NET

FluentValidation is a validation framework based on .NET development. It is open-source, free, and elegant, supporting chained operations, easy to understand, feature-complete, and can be deeply integrated with MVC5, WebApi2, and ASP.NET Core. The component provides over a dozen commonly used validators, good scalability, support for custom validators, and support for localization and multilingual.

Continue Reading
Same category / Same tag 6/20/2024

CodeWF.EventBus: Lightweight Event Bus for Smoother Communication

CodeWF.EventBus is a flexible event bus library that enables decoupled communication between modules. It supports various .NET project types such as WPF, WinForms, ASP.NET Core, etc. With a clean design, it easily implements command publishing and subscribing, as well as requests and responses. Through orderly event handling, it ensures events are properly processed. Simplify your code and improve system maintainability.

Continue Reading