Custom 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 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 APIs project 

create new .Net Code 6.0 Web APIs project

configure your new project

ASP.NET Core Web API

Step 2. Project Structure

Project Structure   

Step 3. Install Package

  • System.IdentityModel.Token.Jwt

Install JWT Package

Step 4. Authenticating user using a Web API endpoint

  • Next, we need to authenticate a user within the application by asking for Credentials and generating 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  UserService class of 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 API Endpoint is called.

    In this scenario, Check user credentials as well as Role Validation. It returns Unathorization with a 401 status code. If the user does not exist and role is matched 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 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 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 AuthenticationService class of Authenticate method to actually generate a token.
  • The code creates token and extracts a string from it that's ready to be returned as bearer token value. 
  • The Token includes 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 example below.  
  • Next, we need to create AuthenticationResponse class used for Token Value set and get. 
    namespace Custom_Jwt_Token_Example.Models
    {
        public class AuthenticateResponse
        {
            public string Token
            {
                get;
                set;
            }
        }
    }
    
  • Next, we need to create AuthenticationRequest class used for username and password sent to 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 for storing 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 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 this point, I have authenticated with the Customer Role. I have restricted only Admin User Accessible to get an unauthorized error.

 

To Change Admin Role Of testUser Username


Similar Articles