diff --git a/MainProject.Tests/Services/JwtService_Tests.cs b/MainProject.Tests/Services/JwtService_Tests.cs index f3db28d..6be0bc5 100644 --- a/MainProject.Tests/Services/JwtService_Tests.cs +++ b/MainProject.Tests/Services/JwtService_Tests.cs @@ -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}"); + } + } + } diff --git a/MainProject.Tests/Services/UserService_Tests.cs b/MainProject.Tests/Services/UserService_Tests.cs index 0aeff48..1993592 100644 --- a/MainProject.Tests/Services/UserService_Tests.cs +++ b/MainProject.Tests/Services/UserService_Tests.cs @@ -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}"); + } + } + } diff --git a/MainProject.Tests/TestsUtils/TestUtils.cs b/MainProject.Tests/TestsUtils/TestUtils.cs index 8eea9a1..129a8af 100644 --- a/MainProject.Tests/TestsUtils/TestUtils.cs +++ b/MainProject.Tests/TestsUtils/TestUtils.cs @@ -68,7 +68,8 @@ public static class TestUtils var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseSqlServer("test"); SqlServerContext sqlServerContext = new SqlServerContext(optionsBuilder.Options); - return new JwtService(configuration, sqlServerContext); + var userServiceMock = new Mock(); + return new JwtService(configuration, sqlServerContext, userServiceMock.Object); } } diff --git a/MainProject.Tests/Utils/JwtTokenUtils_Tests.cs b/MainProject.Tests/Utils/JwtTokenUtils_Tests.cs new file mode 100644 index 0000000..d1a2546 --- /dev/null +++ b/MainProject.Tests/Utils/JwtTokenUtils_Tests.cs @@ -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()); + 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()); + 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()); + 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}"); + } + } + +} + + + + diff --git a/MainProject/Core/Attributes/JwtAuthorizationAttribute .cs b/MainProject/Core/Attributes/JwtAuthorizationAttribute .cs index caeba8d..4fd8d48 100644 --- a/MainProject/Core/Attributes/JwtAuthorizationAttribute .cs +++ b/MainProject/Core/Attributes/JwtAuthorizationAttribute .cs @@ -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(); 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; - - if (string.IsNullOrEmpty(jwtKey) || string.IsNullOrEmpty(jwtIssuer) || string.IsNullOrEmpty(jwtAudience)) + string? headerAuthorization = context.HttpContext.Request.Headers.Authorization.FirstOrDefault(); + + 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); } } } diff --git a/MainProject/Services/JwtService.cs b/MainProject/Services/JwtService.cs index fb962cc..acff447 100644 --- a/MainProject/Services/JwtService.cs +++ b/MainProject/Services/JwtService.cs @@ -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 + 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; } } diff --git a/MainProject/Services/UserService.cs b/MainProject/Services/UserService.cs index 49679f5..eac5ac2 100644 --- a/MainProject/Services/UserService.cs +++ b/MainProject/Services/UserService.cs @@ -8,6 +8,8 @@ namespace BasicDotnetTemplate.MainProject.Services; public interface IUserService { + User? GetUserById(int id); + User? GetUserByGuid(string guid); Task GetUserByUsernameAndPassword(string username, string password); } @@ -28,10 +30,19 @@ public class UserService : BaseService, IUserService private IQueryable 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 GetUserByUsernameAndPassword(string username, string password) { diff --git a/MainProject/Utils/JwtTokenUtils.cs b/MainProject/Utils/JwtTokenUtils.cs new file mode 100644 index 0000000..170da6f --- /dev/null +++ b/MainProject/Utils/JwtTokenUtils.cs @@ -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 + { + 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; + } + +} +