From f80aa1dd950fde00a65552ba4dbd9e5e38e31f65 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Sat, 28 Sep 2024 23:24:27 +0200 Subject: [PATCH] (feature): test updates, jwt secret generation method --- .../Security/JwtGeneratorTests.cs | 16 +++- src/MaksIT.Core/MaksIT.Core.csproj | 2 +- src/MaksIT.Core/Security/JwtGenerator.cs | 86 ++++++++++--------- 3 files changed, 62 insertions(+), 42 deletions(-) diff --git a/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs b/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs index f3c6b82..ba5af52 100644 --- a/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs +++ b/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs @@ -14,7 +14,7 @@ public class JwtGeneratorTests { [Fact] public void GenerateToken_ShouldReturnValidToken() { // Act - var token = JwtGenerator.GenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles); + var (token, jwtTokenClaims) = JwtGenerator.GenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles); // Assert Assert.False(string.IsNullOrEmpty(token)); @@ -23,7 +23,7 @@ public class JwtGeneratorTests { [Fact] public void ValidateToken_ShouldReturnClaimsPrincipal_WhenTokenIsValid() { // Arrange - var token = JwtGenerator.GenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles); + var (token, _) = JwtGenerator.GenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles); // Act var jwtTokenClaims = JwtGenerator.ValidateToken(Secret, Issuer, Audience, token); @@ -55,4 +55,16 @@ public class JwtGeneratorTests { // Assert Assert.False(string.IsNullOrEmpty(refreshToken)); } + + [Fact] + public void GenerateSecret_ShouldReturnDifferentValuesOnSubsequentCalls() { + // Act + string secret1 = JwtGenerator.GenerateSecret(); + string secret2 = JwtGenerator.GenerateSecret(); + + // Assert + Assert.False(string.IsNullOrEmpty(secret1)); + Assert.False(string.IsNullOrEmpty(secret2)); + Assert.NotEqual(secret1, secret2); // Ensure the secrets are unique + } } diff --git a/src/MaksIT.Core/MaksIT.Core.csproj b/src/MaksIT.Core/MaksIT.Core.csproj index 58ba4c4..0f86869 100644 --- a/src/MaksIT.Core/MaksIT.Core.csproj +++ b/src/MaksIT.Core/MaksIT.Core.csproj @@ -8,7 +8,7 @@ MaksIT.Core - 1.0.4 + 1.0.5 Maksym Sadovnychyy MAKS-IT MaksIT.Core diff --git a/src/MaksIT.Core/Security/JwtGenerator.cs b/src/MaksIT.Core/Security/JwtGenerator.cs index 0ff8de7..ea7907c 100644 --- a/src/MaksIT.Core/Security/JwtGenerator.cs +++ b/src/MaksIT.Core/Security/JwtGenerator.cs @@ -1,11 +1,11 @@ -using System.Text; +using System; +using System.Text; +using System.Linq; using System.Security.Claims; using System.Security.Cryptography; using System.IdentityModel.Tokens.Jwt; - using Microsoft.IdentityModel.Tokens; - namespace MaksIT.Core.Security; public class JWTTokenClaims { @@ -15,10 +15,9 @@ public class JWTTokenClaims { public DateTime? ExpiresAt { get; set; } } - public static class JwtGenerator { public static (string, JWTTokenClaims) GenerateToken(string secret, string issuer, string audience, double expiration, string username, List roles) { - var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); + var secretKey = GetSymmetricSecurityKey(secret); var credentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); var issuedAt = DateTime.UtcNow; @@ -26,26 +25,24 @@ public static class JwtGenerator { var claims = new List { - new Claim(ClaimTypes.Name, username), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), - new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(issuedAt).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), - new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(expiresAt).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64) - }; + new Claim(ClaimTypes.Name, username), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(issuedAt).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), + new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(expiresAt).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64) + }; claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); - var token = new JwtSecurityToken( + var tokenDescriptor = new JwtSecurityToken( issuer: issuer, audience: audience, claims: claims, - expires: DateTime.Now.AddMinutes(Convert.ToDouble(expiration)), + expires: expiresAt, signingCredentials: credentials ); - var jwtToken = new JwtSecurityTokenHandler().WriteToken(token); + var jwtToken = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor); - - // Create the JWTTokenClaims object var tokenClaims = new JWTTokenClaims { Username = username, Roles = roles, @@ -56,13 +53,13 @@ public static class JwtGenerator { return (jwtToken, tokenClaims); } - + public static string GenerateSecret(int keySize = 32) => Convert.ToBase64String(GetRandomBytes(keySize)); public static JWTTokenClaims? ValidateToken(string secret, string issuer, string audience, string token) { try { var key = Encoding.UTF8.GetBytes(secret); - var tokenHandler = new JwtSecurityTokenHandler(); + var validationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), @@ -76,34 +73,45 @@ public static class JwtGenerator { var principal = tokenHandler.ValidateToken(token, validationParameters, out var validatedToken); - var username = principal?.Identity?.Name; - var roles = principal?.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList(); + // Validate the algorithm used + if (validatedToken is JwtSecurityToken jwtToken && jwtToken.Header.Alg != SecurityAlgorithms.HmacSha256) + throw new SecurityTokenException("Invalid token algorithm"); - - var issuedAtClaim = principal?.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Iat)?.Value; - var expiresAtClaim = principal?.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Exp)?.Value; - - DateTime? issuedAt = issuedAtClaim != null ? DateTimeOffset.FromUnixTimeSeconds(long.Parse(issuedAtClaim)).UtcDateTime : (DateTime?)null; - DateTime? expiresAt = expiresAtClaim != null ? DateTimeOffset.FromUnixTimeSeconds(long.Parse(expiresAtClaim)).UtcDateTime : (DateTime?)null; - - - return new JWTTokenClaims { - Username = username, - Roles = roles, - IssuedAt = issuedAt, - ExpiresAt = expiresAt - }; + return ExtractClaims(principal); } catch { return null; } } - public static string GenerateRefreshToken() { - var randomNumber = new byte[32]; - using (var rng = RandomNumberGenerator.Create()) { - rng.GetBytes(randomNumber); - return Convert.ToBase64String(randomNumber); - } + public static string GenerateRefreshToken() => Convert.ToBase64String(GetRandomBytes(32)); + + // Private helper method to extract claims + private static JWTTokenClaims? ExtractClaims(ClaimsPrincipal principal) { + var username = principal.Identity?.Name; + var roles = principal.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList(); + + var issuedAtClaim = principal.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Iat)?.Value; + var expiresAtClaim = principal.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Exp)?.Value; + + DateTime? issuedAt = issuedAtClaim != null ? DateTimeOffset.FromUnixTimeSeconds(long.Parse(issuedAtClaim)).UtcDateTime : (DateTime?)null; + DateTime? expiresAt = expiresAtClaim != null ? DateTimeOffset.FromUnixTimeSeconds(long.Parse(expiresAtClaim)).UtcDateTime : (DateTime?)null; + + return new JWTTokenClaims { + Username = username, + Roles = roles, + IssuedAt = issuedAt, + ExpiresAt = expiresAt + }; + } + + // Private helper method to get a symmetric security key + private static SymmetricSecurityKey GetSymmetricSecurityKey(string secret) => new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); + + // Private helper method for generating random bytes + private static byte[] GetRandomBytes(int size) { + var bytes = new byte[size]; + RandomNumberGenerator.Fill(bytes); + return bytes; } }