JWT Token Authentication And Role Authorization Using .Net Core 6.0 Web APIs

Introduction

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 this post, I specifically talk about.

  • Authentication for an ASP.NET Core Web API
  • Using JWT Tokens
  • Using Role-Based Authorization
  • Using only ASP.NET's low-level Auth features - not using ASP.NET Core Identity.

In this post, I follow the Repository Pattern.

Prerequisites

  • .NET Core 6 SDK
  • Visual Studio 2022
  • Postman

Step-by-step Implementation

Step 1. Create a new .Net Core 6.0 Web API project.

Web API

New project

Web

Step 2. Project Structure.

Project Structure

Step 3. Install-Package

System.IdentityModel.Token.Jwt

 Install Package

Step 4. Authenticating users using a Web API endpoint.

  • Next, we need to authenticate a user within the application by asking for Credentials, generating a Token, and returning it to the API client
  • This likely happens in a controller Action Method or Middleware Endpoint Handler. Here's what this looks like using the controller Action Method
    using Custom_Jwt_Token_Example.Models;
    using Custom_Jwt_Token_Example.Services;
    using Microsoft.AspNetCore.Mvc;
    namespace Custom_Jwt_Token_Example.Controllers
    {
        [Route("api/[controller]")]
        public class AuthenticationController : Controller
        {
            private readonly IAuthenticationService authenticationService;
            public AuthenticationController(IAuthenticationService authenticationService) { 
            this.authenticationService = authenticationService; 
            }
            [HttpPost]
            [Route("Login")]
            public AuthenticateResponse Login(AuthenticateRequest model)
            {
                return this.authenticationService.Authenticate(model);
                
            }
        }
    }
    

Implementing the Repository interface

  • Create the IAuthentication interface under the Services Folder.
  • This interface is planned to be an abstraction Layer over the Authentication Functionality.
    using Custom_Jwt_Token_Example.Models;
    namespace Custom_Jwt_Token_Example.Services
    {
        public interface IAuthenticationService
        {
            AuthenticateResponse Authenticate(AuthenticateRequest model);
        }
    }
    
  • Create the IUserService interface under the Services Folder
  • This interface is planned to be an abstraction Layer over the User Functionality.
    using Custom_Jwt_Token_Example.Models;
    namespace Custom_Jwt_Token_Example.Services
    {
        public interface IUserService
        {
            User GetById(int id);
            IEnumerable<User> GetAll();
        }
    }
    
  • I'm using the UserService class of the GetById method to actually Get user details by id.
    using Custom_Jwt_Token_Example.Models;
    namespace Custom_Jwt_Token_Example.Services
    {
        public class UserService : IUserService
        {
            private List<User> _users = new List<User> {
                new User {
                    Id = 1, FirstName = "mytest",Role= new List<Role>{Role.Customer}, LastName = "User", Username = "mytestuser", Password = "test123"
                },
                new User {
                    Id = 2, FirstName = "mytest2", LastName = "User2", Username = "test", Password = "test"
                }
            };
    
            public IEnumerable<User> GetAll()
            {
                return _users;
            }
            public User GetById(int id)
            {
                return _users.FirstOrDefault(x => x.Id == id);
                
            }
        }
    }
    
  • Create a class called AuthenticationService in the Service Folder with the following code.
    using Custom_Jwt_Token_Example.Helper;
    using Custom_Jwt_Token_Example.Models;
    using Microsoft.Extensions.Options;
    using Microsoft.IdentityModel.Tokens;
    using System.IdentityModel.Tokens.Jwt;
    using System.Security.Claims;
    using System.Text;
    
    namespace Custom_Jwt_Token_Example.Services
    {
        public class AuthenticationService : IAuthenticationService
        {
            private List<User> _users = new List<User> {
                new User {
                    Id = 1, FirstName = "mytest", LastName = "User", Username = "mytestuser",Role= new List<Role>{Role.Customer} , Password = "test123"
                }
            };
            private readonly AppSettings _appSettings;
            public AuthenticationService(IOptions<AppSettings> appSettings)
            {
                _appSettings = appSettings.Value;
            }
            public AuthenticateResponse Authenticate(AuthenticateRequest model)
            {
                var user = _users.SingleOrDefault(x => x.Username == model.UserName && x.Password == model.Password);
                if (user == null) return null;
                var token = generateToken(user);
                return new AuthenticateResponse() {Token= token}; 
            }
            private string generateToken(User user)
            {
                var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_appSettings.Key));
                var credetial = new SigningCredentials(securityKey,SecurityAlgorithms.HmacSha256);
                List<Claim> claims = new List<Claim>(){
                        new Claim("Id",Convert.ToString(user.Id)),
                        new Claim(JwtRegisteredClaimNames.Sub, "Test"),
                        new Claim(JwtRegisteredClaimNames.Email, "[email protected]"),
                        //new Claim("Role", Convert.ToString(user.Role)),
                        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
    
                };
                foreach (var role in user.Role) {
    
                    claims.Add(new Claim("Role", Convert.ToString(role)));
                }
                var token = new JwtSecurityToken(_appSettings.Issuer, _appSettings.Issuer, claims, expires: DateTime.UtcNow.AddHours(1), signingCredentials: credetial);
            
                return new JwtSecurityTokenHandler().WriteToken(token);
            }
        }
    }
    
  • First, let's create a new class for our middleware component. In this example, let's assume we want to get a token from the header, validate the token, and attach it to the context of the user variable stored user information.
    using Custom_Jwt_Token_Example.Services;
    using Microsoft.Extensions.Options;
    using Microsoft.IdentityModel.Tokens;
    using System.IdentityModel.Tokens.Jwt;
    using System.Text;
    
    namespace Custom_Jwt_Token_Example.Helper
    {
        public class JwtMiddleware
        {
            private readonly RequestDelegate _next;
            private readonly AppSettings _appSettings;
            public JwtMiddleware(RequestDelegate _next, IOptions<AppSettings> _appSettings)
            {
                this._next = _next;
                this._appSettings = _appSettings.Value;
            }
            public async Task Invoke(HttpContext context, IUserService userService)
            {
    
                var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
                if (token != null)
                    //Validate Token
                    attachUserToContext(context, userService, token);
                _next(context);
            }
            private void attachUserToContext(HttpContext context, IUserService userService, string token)
            {
                try
                {
                    var tokenHandler = new JwtSecurityTokenHandler();
                    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_appSettings.Key));
                    tokenHandler.ValidateToken(token, new TokenValidationParameters
                    {
                        ValidateAudience = true,
                        ValidateIssuer = true,
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = key,
                        ClockSkew = TimeSpan.Zero,
                        ValidIssuer = _appSettings.Issuer,
                        ValidAudience = _appSettings.Issuer
                    }, out SecurityToken validateToken);                
                    var jwtToken = (JwtSecurityToken)validateToken;
                    var userId = int.Parse(jwtToken.Claims.FirstOrDefault(_=>_.Type=="Id").Value);
                    context.Items["User"] = userService.GetById(userId);
                }
                catch (Exception ex)
                {
                }
            }
        }
    }
    
  • When decorating an API endpoint with the [Authorization] attribute, the OnAuthorization method will be called each time before the API Endpoint is called.
  • In this scenario, check user credentials and Role Validation. It returns Unathorization with a 401 status code. If the user does not exist and the role matches with the Authorize attribute,
    using Custom_Jwt_Token_Example.Models;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Filters;
    
    namespace Custom_Jwt_Token_Example.Helper
    {
       [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]    
        public class Authorization : Attribute, IAuthorizationFilter
        {
            private readonly IList<Role> _roles;
            public Authorization(params Role[] _roles) { 
            
                this._roles = _roles?? new Role[]{ };
            }
            public void OnAuthorization(AuthorizationFilterContext context)
            {
                var isRolePermission = false;
                User user = (User)context.HttpContext.Items["User"];
                if (user == null)
                {
                    context.Result = new JsonResult(
                            new { Message = "Unauthorization" }
                        )
                    { StatusCode = StatusCodes.Status401Unauthorized };
                }
                if(user != null && this._roles.Any())
                    foreach (var userRole in user.Role)
                    {
                        foreach (var AuthRole in this._roles)
                        {
    
                            if (userRole == AuthRole)
                            {
                                isRolePermission = true;
                            }
                        }
                    }
                    
                if(!isRolePermission)
                    context.Result = new JsonResult(
                               new { Message = "Unauthorization" }
                           )
                    { StatusCode = StatusCodes.Status401Unauthorized };
            }
        }
    }
  • Add Decorator an API endpoint with [Authorization(Role. Customer)] attribute.
    using Custom_Jwt_Token_Example.Helper;
    using Custom_Jwt_Token_Example.Models;
    using Microsoft.AspNetCore.Mvc;
    
    namespace Custom_Jwt_Token_Example.Controllers
    {
        [Authorization(Role.Customer)]
        [ApiController]
        [Route("[controller]")]
        public class WeatherForecastController : ControllerBase
        {
            private static readonly string[] Summaries = new[]
            {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };
            private readonly ILogger<WeatherForecastController> _logger;
            public WeatherForecastController(ILogger<WeatherForecastController> logger)
            {
                _logger = logger;
            }
            [HttpGet(Name = "GetWeatherForecast")]
            public IEnumerable<WeatherForecast> Get()
            {
                return Enumerable.Range(1, 5).Select(index => new WeatherForecast
                {
                    Date = DateTime.Now.AddDays(index),
                    TemperatureC = Random.Shared.Next(-20, 55),
                    Summary = Summaries[Random.Shared.Next(Summaries.Length)]
                })
                .ToArray();
            }
        }
    }
  • In the Appsetting.Json File, add Appsetting Section JWT Token Key and Issuer.
    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "AllowedHosts": "*",
      "AppSettings": {
        "Key": "986ghgrgtru989ASdsaerew13434545435",
        "Issuer": "TestIssuer"
      }
    }
    

Add Middleware

  • Next, we need to register custom middleware using the Use<> extension method.
    using Custom_Jwt_Token_Example.Helper;
    using Custom_Jwt_Token_Example.Services;
    
    var builder = WebApplication.CreateBuilder(args);
    // Add services to the container.
    builder.Services.AddControllers();
    // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("AppSettings"));
    builder.Services.AddScoped<IAuthenticationService, AuthenticationService>();
    builder.Services.AddScoped<IUserService,UserService>();
    
    var app = builder.Build();
    app.UseMiddleware<JwtMiddleware>();
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    app.UseHttpsRedirection();
    app.UseAuthorization();
    app.MapControllers();
    app.Run();
    
  • I'm using the AuthenticationService class of the Authenticate method to actually generate a token.
  • The code creates a token and extracts a string from it that's ready to be returned as bearer token value.
  • The Token includes a username and role, which is what's required for ASP.Net Core's Authentication to work. I can then add some additional application-specific claims, if necessary, like the Display Name and User State object in the example below.
  • Next, we need to create the AuthenticationResponse class used for the Token Value set and get.
    namespace Custom_Jwt_Token_Example.Models
    {
        public class AuthenticateResponse
        {
            public string Token
            {
                get;
                set;
            }
        }
    }
    
  • Next, we need to create an AuthenticationRequest class used for the username and password sent to an authentication request.
    using System.ComponentModel.DataAnnotations;
    namespace Custom_Jwt_Token_Example.Models
    {
        public class AuthenticateRequest
        {
            [Required]
            public string UserName 
            { 
                get; 
                set;
            }
            [Required]
            public string Password 
            { 
                get; 
                set; 
            }
        }
    }
    
  • Next, we need to create a role enum used to store Admin User and Customer User.
    namespace Custom_Jwt_Token_Example.Models
    {
        public enum Role
        {
            Admin,
            Customer
        }
    }
    
  • Next, we need to create a user class used for user information.
    using System.Text.Json.Serialization;
    namespace Custom_Jwt_Token_Example.Models
    {
        public class User
        {
            public int Id
            {
                get;
                set;
            }
            public string FirstName
            {
                get;
                set;
            }
            public string LastName
            {
                get;
                set;
            }
            public string Username
            {
                get;
                set;
            }
            public List<Role> Role { get; set; }
            [JsonIgnore]
            public string Password
            {
                get;
                set;
            }
        }
    }
    
  • Next, we need to create an Appsetting class used for storing Appsetting Section Key And Issuer Values.
    namespace Custom_Jwt_Token_Example.Helper
    {
        public class AppSettings
        {
            public string Key
            {
                get;
                set;
            }
            public string Issuer
            {
                get;
                set;
            }
        }
    }
    

Valid Token With Role Is Customer

So, at this point, I have authenticated with the Customer Role. I have restricted only Admin User Accessible to get an unauthorized error.

 Customer Role

To Change Admin Role Of testUser Username

TestUser Username


Similar Articles