Code refactoring + tests

This commit is contained in:
2025-03-04 00:42:20 +01:00
parent a0c93ea587
commit f73fe748ed
8 changed files with 299 additions and 86 deletions

View File

@@ -44,7 +44,7 @@ public class JwtService_Tests
}
[TestMethod]
public async Task GenerateToken()
public void GenerateToken()
{
try
{
@@ -52,7 +52,7 @@ public class JwtService_Tests
var testString = "test";
if(jwtService != null)
{
var jwt = jwtService.GenerateToken(testString, testString);
var jwt = jwtService.GenerateToken(testString);
Assert.IsTrue(jwt != null);
Assert.IsInstanceOfType(jwt, typeof(string));
}
@@ -68,6 +68,31 @@ public class JwtService_Tests
}
}
[TestMethod]
public void ValidateToken_Null()
{
try
{
var jwtService = TestUtils.CreateJwtService();
var testString = "test";
if(jwtService != null)
{
var jwt = jwtService.GenerateToken(testString);
var user = jwtService.ValidateToken($"Bearer {jwt}");
Assert.IsTrue(user == null);
}
else
{
Assert.Fail($"JwtService is null");
}
}
catch (Exception ex)
{
Console.WriteLine(ex.InnerException);
Assert.Fail($"An exception was thrown: {ex.Message}");
}
}
}

View File

@@ -46,7 +46,7 @@ public class UserService_Tests
}
[TestMethod]
public async Task GetUsers()
public async Task GetUserByUsernameAndPassword_Null()
{
try
{
@@ -69,6 +69,33 @@ public class UserService_Tests
}
}
// TODO
// [TestMethod]
public async Task GetUserByUsernameAndPassword_Success()
{
try
{
var userService = TestUtils.CreateUserService();
var testUsername = "test@email.it";
var testPassword = "password";
if(userService != null)
{
var user = await userService.GetUserByUsernameAndPassword(testUsername, testPassword);
Assert.IsTrue(user != null);
Assert.IsTrue(user.Username == testUsername);
}
else
{
Assert.Fail($"UserService is null");
}
}
catch (Exception ex)
{
Console.WriteLine(ex.InnerException);
Assert.Fail($"An exception was thrown: {ex.Message}");
}
}
}

View File

@@ -68,7 +68,8 @@ public static class TestUtils
var optionsBuilder = new DbContextOptionsBuilder<SqlServerContext>();
optionsBuilder.UseSqlServer("test");
SqlServerContext sqlServerContext = new SqlServerContext(optionsBuilder.Options);
return new JwtService(configuration, sqlServerContext);
var userServiceMock = new Mock<IUserService>();
return new JwtService(configuration, sqlServerContext, userServiceMock.Object);
}
}

View File

@@ -0,0 +1,76 @@
using System;
using BasicDotnetTemplate.MainProject.Models.Settings;
using Microsoft.AspNetCore.Builder;
using BasicDotnetTemplate.MainProject.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
namespace BasicDotnetTemplate.MainProject.Tests;
[TestClass]
public class JwtTokenUtils_Tests
{
private static string _guid = "15e4be58-e655-475e-b4b8-a9779b359f57";
[TestMethod]
public void GenerateToken()
{
try
{
WebApplicationBuilder builder = WebApplication.CreateBuilder(Array.Empty<string>());
AppSettings appSettings = ProgramUtils.AddConfiguration(ref builder, System.AppDomain.CurrentDomain.BaseDirectory + "/JsonData");
JwtTokenUtils jwtUtils = new JwtTokenUtils(appSettings);
var jwt = jwtUtils.GenerateToken(_guid);
Assert.IsTrue(!String.IsNullOrEmpty(jwt));
}
catch (Exception ex)
{
Console.WriteLine(ex.InnerException);
Assert.Fail($"An exception was thrown: {ex.Message}");
}
}
[TestMethod]
public void ValidateToken()
{
try
{
WebApplicationBuilder builder = WebApplication.CreateBuilder(Array.Empty<string>());
AppSettings appSettings = ProgramUtils.AddConfiguration(ref builder, System.AppDomain.CurrentDomain.BaseDirectory + "/JsonData");
JwtTokenUtils jwtUtils = new JwtTokenUtils(appSettings);
var jwt = jwtUtils.GenerateToken(_guid);
var guid = jwtUtils.ValidateToken($"Bearer {jwt}");
Assert.IsTrue(_guid == guid);
}
catch (Exception ex)
{
Console.WriteLine(ex.InnerException);
Assert.Fail($"An exception was thrown: {ex.Message}");
}
}
[TestMethod]
public void ValidateToken_Empty()
{
try
{
WebApplicationBuilder builder = WebApplication.CreateBuilder(Array.Empty<string>());
AppSettings appSettings = ProgramUtils.AddConfiguration(ref builder, System.AppDomain.CurrentDomain.BaseDirectory + "/JsonData");
JwtTokenUtils jwtUtils = new JwtTokenUtils(appSettings);
var jwt = jwtUtils.GenerateToken(_guid);
var guid = jwtUtils.ValidateToken(jwt);
Assert.IsTrue(String.IsNullOrEmpty(guid));
}
catch (Exception ex)
{
Console.WriteLine(ex.InnerException);
Assert.Fail($"An exception was thrown: {ex.Message}");
}
}
}

View File

@@ -9,22 +9,32 @@ using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using BasicDotnetTemplate.MainProject.Models.Settings;
using BasicDotnetTemplate.MainProject.Services;
using DatabaseSqlServer = BasicDotnetTemplate.MainProject.Models.Database.SqlServer;
namespace BasicDotnetTemplate.MainProject.Core.Attributes
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class JwtAuthorizationAttribute : Attribute, IAuthorizationFilter
{
private readonly string? _policyName;
private readonly IJwtService _jwtService;
public JwtAuthorizationAttribute() { }
public JwtAuthorizationAttribute(string policyName)
public JwtAuthorizationAttribute(
IJwtService jwtService
)
{
_policyName = policyName;
_jwtService = jwtService;
}
public static void Unauthorized(AuthorizationFilterContext context)
{
context.Result = new UnauthorizedResult();
}
public void OnAuthorization(AuthorizationFilterContext context)
{
DatabaseSqlServer.User? user = null;
// If [AllowAnonymous], skip
if (context.ActionDescriptor.EndpointMetadata.Any(em => em is AllowAnonymousAttribute))
{
@@ -34,61 +44,19 @@ namespace BasicDotnetTemplate.MainProject.Core.Attributes
var configuration = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
var appSettings = new AppSettings();
configuration.GetSection("AppSettings").Bind(appSettings);
var jwtKey = appSettings.JwtSettings?.Secret ?? String.Empty;
var jwtIssuer = appSettings.JwtSettings?.ValidIssuer ?? String.Empty;
var jwtAudience = appSettings.JwtSettings?.ValidAudience ?? String.Empty;
string? token = null;
string? headerAuthorization = context.HttpContext.Request.Headers.Authorization.FirstOrDefault();
if (string.IsNullOrEmpty(jwtKey) || string.IsNullOrEmpty(jwtIssuer) || string.IsNullOrEmpty(jwtAudience))
if(!String.IsNullOrEmpty(headerAuthorization))
{
context.Result = new UnauthorizedResult();
return;
}
string[]? authorizations = context.HttpContext.Request.Headers.Authorization.FirstOrDefault()?.Split(" ");
if (authorizations != null && authorizations.Length == 2)
{
token = authorizations[1];
}
if (token == null)
{
context.Result = new UnauthorizedResult();
return;
}
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(jwtKey);
tokenHandler.ValidateToken(token, new TokenValidationParameters
user = _jwtService.ValidateToken(headerAuthorization!);
if(user == null)
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidIssuer = jwtIssuer,
ValidateAudience = true,
ValidAudience = jwtAudience,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
if (_policyName != null)
{
var claim = jwtToken.Claims.FirstOrDefault(c => c.Type == _policyName);
if (claim == null)
{
context.Result = new ForbidResult();
return;
}
Unauthorized(context);
}
}
catch
else
{
context.Result = new UnauthorizedResult();
Unauthorized(context);
}
}
}

View File

@@ -5,52 +5,48 @@ using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using BasicDotnetTemplate.MainProject.Core.Database;
using BasicDotnetTemplate.MainProject.Utils;
using DatabaseSqlServer = BasicDotnetTemplate.MainProject.Models.Database.SqlServer;
namespace BasicDotnetTemplate.MainProject.Services;
public interface IJwtService
{
string GenerateToken(string guid);
DatabaseSqlServer.User? ValidateToken(string headerAuthorization);
}
public class JwtService : BaseService, IJwtService
{
private readonly string _jwtKey;
private readonly string _jwtIssuer;
private readonly string _jwtAudience;
private readonly JwtTokenUtils _jwtTokenUtils;
private readonly IUserService _userService;
public JwtService(
IConfiguration configuration,
SqlServerContext sqlServerContext
SqlServerContext sqlServerContext,
IUserService userService
) : base(configuration, sqlServerContext)
{
_jwtKey = _appSettings?.JwtSettings?.Secret ?? String.Empty;
_jwtIssuer = _appSettings?.JwtSettings?.ValidIssuer ?? String.Empty;
_jwtAudience = _appSettings?.JwtSettings?.ValidAudience ?? String.Empty;
_jwtTokenUtils = new JwtTokenUtils(_appSettings);
_userService = userService;
}
public string GenerateToken(string userId, string username)
public string GenerateToken(string guid)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtKey));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var expiration = _appSettings?.JwtSettings?.ExpiredAfterMinsOfInactivity ?? 15;
return _jwtTokenUtils.GenerateToken(guid);
}
var claims = new List<Claim>
public DatabaseSqlServer.User? ValidateToken(string headerAuthorization)
{
DatabaseSqlServer.User? user = null;
string? guid = _jwtTokenUtils.ValidateToken(headerAuthorization);
if(!String.IsNullOrEmpty(guid))
{
new Claim(JwtRegisteredClaimNames.Sub, userId),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim("userid", userId)
};
var token = new JwtSecurityToken(
_jwtIssuer,
_jwtAudience,
claims,
expires: DateTime.Now.AddMinutes(expiration),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
user = this._userService.GetUserByGuid(guid);
}
return user;
}
}

View File

@@ -8,6 +8,8 @@ namespace BasicDotnetTemplate.MainProject.Services;
public interface IUserService
{
User? GetUserById(int id);
User? GetUserByGuid(string guid);
Task<User?> GetUserByUsernameAndPassword(string username, string password);
}
@@ -28,10 +30,19 @@ public class UserService : BaseService, IUserService
private IQueryable<User> GetUserByUsername(string username)
{
return this.GetUsers().Where(x =>
String.Equals(x.Username, username, StringComparison.CurrentCultureIgnoreCase)
);
x.Username.ToString() == username.ToString()
);
}
public User? GetUserById(int id)
{
return this.GetUsers().Where(x => x.Id == id).FirstOrDefault();
}
public User? GetUserByGuid(string guid)
{
return this.GetUsers().Where(x => x.Guid == guid).FirstOrDefault();
}
public async Task<User?> GetUserByUsernameAndPassword(string username, string password)
{

View File

@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using BasicDotnetTemplate.MainProject.Models.Settings;
using DatabaseSqlServer = BasicDotnetTemplate.MainProject.Models.Database.SqlServer;
namespace BasicDotnetTemplate.MainProject.Utils;
public class JwtTokenUtils
{
private readonly string _jwtKey;
private readonly string _jwtIssuer;
private readonly string _jwtAudience;
private readonly int _expiration;
private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger();
public JwtTokenUtils(AppSettings appSettings)
{
_jwtKey = appSettings?.JwtSettings?.Secret ?? String.Empty;
_jwtIssuer = appSettings?.JwtSettings?.ValidIssuer ?? String.Empty;
_jwtAudience = appSettings?.JwtSettings?.ValidAudience ?? String.Empty;
_expiration = appSettings?.JwtSettings?.ExpiredAfterMinsOfInactivity ?? 15;
}
public string GenerateToken(string guid)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtKey));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, guid),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim("guid", guid)
};
var token = new JwtSecurityToken(
_jwtIssuer,
_jwtAudience,
claims,
expires: DateTime.Now.AddMinutes(_expiration),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public string? ValidateToken(string headerAuthorization)
{
string? token = null;
string? guid = null;
if (
String.IsNullOrEmpty(_jwtKey) ||
String.IsNullOrEmpty(_jwtIssuer) ||
String.IsNullOrEmpty(_jwtAudience)
)
{
return guid;
}
string[]? authorizations = headerAuthorization.Split(" ");
if (authorizations != null && authorizations.Length == 2)
{
token = authorizations[1];
}
if(!String.IsNullOrEmpty(token))
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_jwtKey);
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidIssuer = _jwtIssuer,
ValidateAudience = true,
ValidAudience = _jwtAudience,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
if (jwtToken != null)
{
var claimedUserId = jwtToken.Claims.FirstOrDefault(c => c.Type == "guid");
if (claimedUserId != null && !String.IsNullOrEmpty(claimedUserId.Value))
{
guid = claimedUserId.Value;
}
}
}
catch
{
return guid;
}
}
return guid;
}
}