From e4da2e68b3dfedd5ea5cc3fa3ab5849d9ca39b99 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Thu, 26 Sep 2024 21:02:14 +0200 Subject: [PATCH] (feature): JWT token handle methods --- README.md | 52 ++++++++++++ .../Security/JwtGeneratorTests.cs | 58 ++++++++++++++ src/MaksIT.Core/MaksIT.Core.csproj | 4 +- src/MaksIT.Core/Security/JwtGenerator.cs | 80 +++++++++++++++++++ 4 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs create mode 100644 src/MaksIT.Core/Security/JwtGenerator.cs diff --git a/README.md b/README.md index 0cc5f5f..dfa5822 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ MaksIT.Core is a collection of helper methods and extensions for .NET projects, - [Password Hasher](#password-hasher) - [DataTable Extensions](#datatable-extensions) - [DateTime Extensions](#datetime-extensions) + - [JWT Token Generation and Validation](#jwt-token-generation-and-validation) - [Available Methods](#available-methods) - [Enumeration Methods](#enumeration-methods) - [Guid Methods](#guid-methods) @@ -21,6 +22,7 @@ MaksIT.Core is a collection of helper methods and extensions for .NET projects, - [Security Methods](#security-methods) - [DataTable Methods](#datatable-methods) - [DateTime Methods](#datetime-methods) + - [JWT Methods](#jwt-methods) - [Contributing](#contributing) - [License](#license) - [Contact](#contact) @@ -166,6 +168,50 @@ DateTime nextThursday = DateTime.Now.NextWeekday(DayOfWeek.Thursday); Console.WriteLine(nextThursday); // Output: Date of the next Thursday ``` +### JWT Token Generation and Validation + +The `JwtGenerator` class provides methods for generating and validating JWT tokens, as well as generating refresh tokens. This is useful for implementing secure authentication and authorization mechanisms in your applications. + +#### Generate a JWT Token + +```csharp +using MaksIT.Core.Security; + +string secret = "your_secret_key"; +string issuer = "your_issuer"; +string audience = "your_audience"; +double expiration = 30; // Token expiration in minutes +string username = "user123"; +List roles = new List { "Admin", "User" }; + +string token = JwtGenerator.GenerateToken(secret, issuer, audience, expiration, username, roles); +Console.WriteLine("Generated JWT Token: " + token); +``` + +#### Validate a JWT Token + +```csharp +string tokenToValidate = "your_jwt_token"; + +JWTTokenClaims? claims = JwtGenerator.ValidateToken(secret, issuer, audience, tokenToValidate); +if (claims != null) +{ + Console.WriteLine($"Username: {claims.Username}"); + Console.WriteLine("Roles: " + string.Join(", ", claims.Roles)); +} +else +{ + Console.WriteLine("Invalid token."); +} +``` + +#### Generate a Refresh Token + +```csharp +string refreshToken = JwtGenerator.GenerateRefreshToken(); +Console.WriteLine("Generated Refresh Token: " + refreshToken); +``` + ## Available Methods ### Enumeration Methods @@ -241,6 +287,12 @@ Console.WriteLine(nextThursday); // Output: Date of the next Thursday - **IsSameMonth(this DateTime date, DateTime targetDate)**: Checks if two dates are in the same month and year. - **GetDifferenceInYears(this DateTime startDate, DateTime endDate)**: Returns the difference in years between two dates. +### JWT Methods + +- **GenerateToken**: Generates jwt-token-generation-and-validationa JWT token. +- **ValidateToken**: Validates a JWT token and extracts claims. +- **GenerateRefreshToken**: Generates a secure refresh token. + ## Contributing Contributions to this project are welcome! Please fork the repository and submit a pull request with your changes. If you encounter any issues or have feature requests, feel free to open an issue on GitHub. diff --git a/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs b/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs new file mode 100644 index 0000000..f3c6b82 --- /dev/null +++ b/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs @@ -0,0 +1,58 @@ +using MaksIT.Core.Security; + + +namespace MaksIT.Core.Tests.Security; + +public class JwtGeneratorTests { + private const string Secret = "supersecretkey12345678901234567890"; + private const string Issuer = "testIssuer"; + private const string Audience = "testAudience"; + private const double Expiration = 30; // 30 minutes + private const string Username = "testUser"; + private readonly List Roles = new List { "Admin", "User" }; + + [Fact] + public void GenerateToken_ShouldReturnValidToken() { + // Act + var token = JwtGenerator.GenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles); + + // Assert + Assert.False(string.IsNullOrEmpty(token)); + } + + [Fact] + public void ValidateToken_ShouldReturnClaimsPrincipal_WhenTokenIsValid() { + // Arrange + var token = JwtGenerator.GenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles); + + // Act + var jwtTokenClaims = JwtGenerator.ValidateToken(Secret, Issuer, Audience, token); + + // Assert + Assert.NotNull(jwtTokenClaims); + Assert.Equal(Username, jwtTokenClaims.Username); + Assert.Contains(jwtTokenClaims.Roles ?? new List(), c => c == "Admin"); + Assert.Contains(jwtTokenClaims.Roles ?? new List(), c => c == "User"); + } + + [Fact] + public void ValidateToken_ShouldReturnNull_WhenTokenIsInvalid() { + // Arrange + var invalidToken = "invalidToken"; + + // Act + var principal = JwtGenerator.ValidateToken(Secret, Issuer, Audience, invalidToken); + + // Assert + Assert.Null(principal); + } + + [Fact] + public void GenerateRefreshToken_ShouldReturnNonEmptyString() { + // Act + var refreshToken = JwtGenerator.GenerateRefreshToken(); + + // Assert + Assert.False(string.IsNullOrEmpty(refreshToken)); + } +} diff --git a/src/MaksIT.Core/MaksIT.Core.csproj b/src/MaksIT.Core/MaksIT.Core.csproj index 42b7754..30160a0 100644 --- a/src/MaksIT.Core/MaksIT.Core.csproj +++ b/src/MaksIT.Core/MaksIT.Core.csproj @@ -8,7 +8,7 @@ MaksIT.Core - 1.0.2 + 1.0.3 Maksym Sadovnychyy MAKS-IT MaksIT.Core @@ -26,5 +26,7 @@ + + diff --git a/src/MaksIT.Core/Security/JwtGenerator.cs b/src/MaksIT.Core/Security/JwtGenerator.cs new file mode 100644 index 0000000..7ad1857 --- /dev/null +++ b/src/MaksIT.Core/Security/JwtGenerator.cs @@ -0,0 +1,80 @@ +using System.Text; +using System.Security.Claims; +using System.Security.Cryptography; +using System.IdentityModel.Tokens.Jwt; + +using Microsoft.IdentityModel.Tokens; + + +namespace MaksIT.Core.Security; + +public class JWTTokenClaims { + public required string? Username { get; set; } + public required List? Roles { get; set; } +} + + +public static class JwtGenerator { + public static string GenerateToken(string secret, string issuer, string audience, double expiration, string username, List roles) { + var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); + var credentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); + + var claims = new List + { + new Claim(ClaimTypes.Name, username), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); + + var token = new JwtSecurityToken( + issuer: issuer, + audience: audience, + claims: claims, + expires: DateTime.Now.AddMinutes(Convert.ToDouble(expiration)), + signingCredentials: credentials + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + + + 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), + ValidateIssuer = true, + ValidIssuer = issuer, + ValidateAudience = true, + ValidAudience = audience, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }; + + 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(); + + return new JWTTokenClaims { + Username = username, + Roles = roles + }; + } + catch { + return null; + } + } + public static string GenerateRefreshToken() { + var randomNumber = new byte[32]; + using (var rng = RandomNumberGenerator.Create()) { + rng.GetBytes(randomNumber); + return Convert.ToBase64String(randomNumber); + } + } +}