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] [TestMethod]
public async Task GenerateToken() public void GenerateToken()
{ {
try try
{ {
@@ -52,7 +52,7 @@ public class JwtService_Tests
var testString = "test"; var testString = "test";
if(jwtService != null) if(jwtService != null)
{ {
var jwt = jwtService.GenerateToken(testString, testString); var jwt = jwtService.GenerateToken(testString);
Assert.IsTrue(jwt != null); Assert.IsTrue(jwt != null);
Assert.IsInstanceOfType(jwt, typeof(string)); 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] [TestMethod]
public async Task GetUsers() public async Task GetUserByUsernameAndPassword_Null()
{ {
try 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>(); var optionsBuilder = new DbContextOptionsBuilder<SqlServerContext>();
optionsBuilder.UseSqlServer("test"); optionsBuilder.UseSqlServer("test");
SqlServerContext sqlServerContext = new SqlServerContext(optionsBuilder.Options); 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 System.Text;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using BasicDotnetTemplate.MainProject.Models.Settings; using BasicDotnetTemplate.MainProject.Models.Settings;
using BasicDotnetTemplate.MainProject.Services;
using DatabaseSqlServer = BasicDotnetTemplate.MainProject.Models.Database.SqlServer;
namespace BasicDotnetTemplate.MainProject.Core.Attributes namespace BasicDotnetTemplate.MainProject.Core.Attributes
{ {
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class JwtAuthorizationAttribute : Attribute, IAuthorizationFilter public class JwtAuthorizationAttribute : Attribute, IAuthorizationFilter
{ {
private readonly string? _policyName; private readonly IJwtService _jwtService;
public JwtAuthorizationAttribute() { } public JwtAuthorizationAttribute(
public JwtAuthorizationAttribute(string policyName) IJwtService jwtService
)
{ {
_policyName = policyName; _jwtService = jwtService;
}
public static void Unauthorized(AuthorizationFilterContext context)
{
context.Result = new UnauthorizedResult();
} }
public void OnAuthorization(AuthorizationFilterContext context) public void OnAuthorization(AuthorizationFilterContext context)
{ {
DatabaseSqlServer.User? user = null;
// If [AllowAnonymous], skip // If [AllowAnonymous], skip
if (context.ActionDescriptor.EndpointMetadata.Any(em => em is AllowAnonymousAttribute)) 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 configuration = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
var appSettings = new AppSettings(); var appSettings = new AppSettings();
configuration.GetSection("AppSettings").Bind(appSettings); configuration.GetSection("AppSettings").Bind(appSettings);
var jwtKey = appSettings.JwtSettings?.Secret ?? String.Empty; string? headerAuthorization = context.HttpContext.Request.Headers.Authorization.FirstOrDefault();
var jwtIssuer = appSettings.JwtSettings?.ValidIssuer ?? String.Empty;
var jwtAudience = appSettings.JwtSettings?.ValidAudience ?? String.Empty;
string? token = null;
if (string.IsNullOrEmpty(jwtKey) || string.IsNullOrEmpty(jwtIssuer) || string.IsNullOrEmpty(jwtAudience)) if(!String.IsNullOrEmpty(headerAuthorization))
{ {
context.Result = new UnauthorizedResult(); user = _jwtService.ValidateToken(headerAuthorization!);
return; if(user == null)
}
string[]? authorizations = context.HttpContext.Request.Headers.Authorization.FirstOrDefault()?.Split(" ");
if (authorizations != null && authorizations.Length == 2)
{ {
token = authorizations[1]; Unauthorized(context);
} }
}
if (token == null) else
{ {
context.Result = new UnauthorizedResult(); Unauthorized(context);
return;
}
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 (_policyName != null)
{
var claim = jwtToken.Claims.FirstOrDefault(c => c.Type == _policyName);
if (claim == null)
{
context.Result = new ForbidResult();
return;
}
}
}
catch
{
context.Result = new UnauthorizedResult();
} }
} }
} }

View File

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

View File

@@ -8,6 +8,8 @@ namespace BasicDotnetTemplate.MainProject.Services;
public interface IUserService public interface IUserService
{ {
User? GetUserById(int id);
User? GetUserByGuid(string guid);
Task<User?> GetUserByUsernameAndPassword(string username, string password); Task<User?> GetUserByUsernameAndPassword(string username, string password);
} }
@@ -28,10 +30,19 @@ public class UserService : BaseService, IUserService
private IQueryable<User> GetUserByUsername(string username) private IQueryable<User> GetUserByUsername(string username)
{ {
return this.GetUsers().Where(x => 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) 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;
}
}