(feature): jwt generator update and dto doc comparison

This commit is contained in:
Maksym Sadovnychyy 2025-06-28 22:57:42 +02:00
parent bbe4000095
commit e671932669
6 changed files with 122 additions and 31 deletions

View File

@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using Xunit; using Xunit;
using MaksIT.Core.Security; using MaksIT.Core.Security.JWT;
namespace MaksIT.Core.Tests.Security { namespace MaksIT.Core.Tests.Security {
public class JwtGeneratorTests { public class JwtGeneratorTests {

View File

@ -2,4 +2,20 @@
public abstract class DtoDocumentBase<T> : DtoObjectBase { public abstract class DtoDocumentBase<T> : DtoObjectBase {
public required T Id { get; set; } public required T Id { get; set; }
// Override Equals to compare based on Id
public override bool Equals(object? obj) {
if (obj is DtoDocumentBase<T> other) // Compare with the same base type
return EqualityComparer<T>.Default.Equals(Id, other.Id); // Use EqualityComparer for generic type comparison
return false;
}
// Override GetHashCode to use Id
public override int GetHashCode() {
if (Id is null)
throw new InvalidOperationException("Id cannot be null when generating hash code.");
return EqualityComparer<T>.Default.GetHashCode(Id); // Use EqualityComparer for hash code generation
}
} }

View File

@ -8,7 +8,7 @@
<!-- NuGet package metadata --> <!-- NuGet package metadata -->
<PackageId>MaksIT.Core</PackageId> <PackageId>MaksIT.Core</PackageId>
<Version>1.4.0</Version> <Version>1.4.1</Version>
<Authors>Maksym Sadovnychyy</Authors> <Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company> <Company>MAKS-IT</Company>
<Product>MaksIT.Core</Product> <Product>MaksIT.Core</Product>

View File

@ -0,0 +1,29 @@
namespace MaksIT.Core.Security.JWT;
public class JWTTokenClaims {
/// <summary>
/// Gets or sets the unique identifier for the user.
/// </summary>
public string? UserId { get; set; }
/// <summary>
/// Gets or sets the username associated with the user.
/// </summary>
public string? Username { get; set; }
/// <summary>
/// Gets or sets the list of roles associated with the current entity.
/// </summary>
public List<string>? Roles { get; set; }
/// <summary>
/// Gets or sets the date and time when the token was issued.
/// </summary>
public DateTime? IssuedAt { get; set; }
/// <summary>
/// Gets or sets the date and time when the token expires.
/// </summary>
public DateTime? ExpiresAt { get; set; }
}

View File

@ -0,0 +1,40 @@
namespace MaksIT.Core.Security.JWT;
public class JWTTokenGenerateRequest {
/// <summary>
/// Secret key used for signing the JWT token. Must be kept secure and not shared publicly.
/// </summary>
public required string Secret { get; set; }
/// <summary>
/// Issuer of the JWT token, typically the application or service that generates the token.
/// </summary>
public required string Issuer { get; set; }
/// <summary>
/// Gets or sets the audience for the application.
/// </summary>
public required string Audience { get; set; }
/// <summary>
/// Expiration time in minutes.
/// </summary>
public double Expiration { get; set; }
/// <summary>
/// Gets or sets the unique identifier for the user.
/// </summary>
public string? UserId { get; set; }
/// <summary>
/// Gets or sets the username associated with the user.
/// </summary>
public string? Username { get; set; }
/// <summary>
/// Gets or sets the list of roles associated with the current entity.
/// </summary>
public List<string>? Roles { get; set; }
}

View File

@ -1,35 +1,33 @@
using System.Text; using Microsoft.IdentityModel.Tokens;
using System.Diagnostics.CodeAnalysis;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.IdentityModel.Tokens.Jwt; using System.Text;
using System.Diagnostics.CodeAnalysis;
using Microsoft.IdentityModel.Tokens;
namespace MaksIT.Core.Security;
public class JWTTokenClaims { namespace MaksIT.Core.Security.JWT;
public string? UserId { get; set; }
public string? Username { get; set; }
public List<string>? Roles { get; set; }
public DateTime? IssuedAt { get; set; }
public DateTime? ExpiresAt { get; set; }
}
public class JWTTokenGenerateRequest {
public required string Secret { get; set; }
public required string Issuer { get; set; }
public required string Audience { get; set; }
public double Expiration { get; set; }
public string? UserId { get; set; }
public string? Username { get; set; }
public List<string>? Roles { get; set; }
}
public static class JwtGenerator { public static class JwtGenerator {
/// <summary>
/// Attempts to generate a JWT token using the specified request parameters.
/// </summary>
/// <param name="request">
/// The <see cref="JWTTokenGenerateRequest"/> containing the secret, issuer, audience, expiration, user ID, username, and roles for the token.
/// </param>
/// <param name="tokenData">
/// When this method returns <c>true</c>, contains a tuple with the generated JWT string and its associated claims; otherwise, <c>null</c>.
/// </param>
/// <param name="errorMessage">
/// When this method returns <c>false</c>, contains the error message describing the failure; otherwise, <c>null</c>.
/// </param>
/// <returns>
/// <c>true</c> if the token was successfully generated; otherwise, <c>false</c>.
/// </returns>
public static bool TryGenerateToken(JWTTokenGenerateRequest request, [NotNullWhen(true)] out (string, JWTTokenClaims)? tokenData, [NotNullWhen(false)] out string? errorMessage) { public static bool TryGenerateToken(JWTTokenGenerateRequest request, [NotNullWhen(true)] out (string, JWTTokenClaims)? tokenData, [NotNullWhen(false)] out string? errorMessage) {
try { try {
var secretKey = GetSymmetricSecurityKey(request.Secret); var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(request.Secret));
var credentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); var credentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var issuedAt = DateTime.UtcNow; var issuedAt = DateTime.UtcNow;
@ -84,6 +82,17 @@ public static class JwtGenerator {
public static string GenerateSecret(int keySize = 32) => Convert.ToBase64String(GetRandomBytes(keySize)); public static string GenerateSecret(int keySize = 32) => Convert.ToBase64String(GetRandomBytes(keySize));
/// <summary>
/// Attempts to validate a JWT token using the provided secret, issuer, audience, and token string.
/// </summary>
/// <param name="secret"></param>
/// <param name="issuer"></param>
/// <param name="audience"></param>
/// <param name="token"></param>
/// <param name="tokenClaims"></param>
/// <param name="errorMessage"></param>
/// <returns></returns>
/// <exception cref="SecurityTokenException"></exception>
public static bool TryValidateToken( public static bool TryValidateToken(
string secret, string secret,
string issuer, string issuer,
@ -136,8 +145,8 @@ public static class JwtGenerator {
var issuedAtClaim = principal.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Iat)?.Value; var issuedAtClaim = principal.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Iat)?.Value;
var expiresAtClaim = principal.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Exp)?.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? issuedAt = issuedAtClaim != null ? DateTimeOffset.FromUnixTimeSeconds(long.Parse(issuedAtClaim)).UtcDateTime : null;
DateTime? expiresAt = expiresAtClaim != null ? DateTimeOffset.FromUnixTimeSeconds(long.Parse(expiresAtClaim)).UtcDateTime : (DateTime?)null; DateTime? expiresAt = expiresAtClaim != null ? DateTimeOffset.FromUnixTimeSeconds(long.Parse(expiresAtClaim)).UtcDateTime : null;
return new JWTTokenClaims { return new JWTTokenClaims {
UserId = userId, UserId = userId,
@ -148,9 +157,6 @@ public static class JwtGenerator {
}; };
} }
// 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 helper method for generating random bytes
private static byte[] GetRandomBytes(int size) { private static byte[] GetRandomBytes(int size) {
var bytes = new byte[size]; var bytes = new byte[size];