In this post, I specifically talk about.
In this post, I follow the Repository Pattern.
- 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"
}
}
- 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;
}
}
}
So, at this point, I have authenticated with the Customer Role. I have restricted only Admin User Accessible to get an unauthorized error.