maksit-core/src/MaksIT.Core/Security/JwtGenerator.cs

161 lines
5.6 KiB
C#

using System.Text;
using System.Security.Claims;
using System.Security.Cryptography;
using System.IdentityModel.Tokens.Jwt;
using System.Diagnostics.CodeAnalysis;
using Microsoft.IdentityModel.Tokens;
namespace MaksIT.Core.Security;
public class JWTTokenClaims {
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 bool TryGenerateToken(JWTTokenGenerateRequest request, [NotNullWhen(true)] out (string, JWTTokenClaims)? tokenData, [NotNullWhen(false)] out string? errorMessage) {
try {
var secretKey = GetSymmetricSecurityKey(request.Secret);
var credentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var issuedAt = DateTime.UtcNow;
var expiresAt = issuedAt.AddMinutes(request.Expiration);
var claims = new List<Claim>
{
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)
};
if (request.UserId != null)
claims.Add(new Claim(ClaimTypes.NameIdentifier, request.UserId));
if (request.Username != null)
claims.Add(new Claim(ClaimTypes.Name, request.Username));
if (request.Roles !=null)
claims.AddRange(request.Roles.Select(role => new Claim(ClaimTypes.Role, role)));
var tokenDescriptor = new JwtSecurityToken(
issuer: request.Issuer,
audience: request.Audience,
claims: claims,
expires: expiresAt,
signingCredentials: credentials
);
var jwtToken = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
var tokenClaims = new JWTTokenClaims {
UserId = request.UserId,
Username = request.Username,
IssuedAt = issuedAt,
ExpiresAt = expiresAt
};
if (request.Roles != null)
tokenClaims.Roles = request.Roles;
tokenData = (jwtToken, tokenClaims);
errorMessage = null;
return true;
}
catch (Exception ex) {
tokenData = null;
errorMessage = ex.Message;
return false;
}
}
public static string GenerateSecret(int keySize = 32) => Convert.ToBase64String(GetRandomBytes(keySize));
public static bool TryValidateToken(
string secret,
string issuer,
string audience,
string token,
out JWTTokenClaims? tokenClaims,
[NotNullWhen(false)] out string? errorMessage
) {
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);
// Validate the algorithm used
if (validatedToken is JwtSecurityToken jwtToken && jwtToken.Header.Alg != SecurityAlgorithms.HmacSha256)
throw new SecurityTokenException("Invalid token algorithm");
tokenClaims = ExtractClaims(principal);
errorMessage = null;
return true;
}
catch (Exception ex) {
tokenClaims = null;
errorMessage = ex.Message;
return false;
}
}
public static string GenerateRefreshToken() => Convert.ToBase64String(GetRandomBytes(32));
// Private helper method to extract claims
private static JWTTokenClaims? ExtractClaims(ClaimsPrincipal principal) {
var userId = principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
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 {
UserId = userId,
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;
}
}