using System.Security.Claims;
using System.Text;
using Domain.Exceptions;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
namespace Infrastructure.Jwt;
///
/// Generates and validates HMAC-SHA256 signed JWTs using .
///
public class JwtInfrastructure : ITokenInfrastructure
{
///
/// Generates a signed JWT containing the user's ID, username, issued-at time, expiry time,
/// and a unique token identifier (JTI), signed using HMAC-SHA256 with the provided secret.
///
///
/// Sets the following registered claims: sub (userId), unique_name (username),
/// iat (current UTC time), exp (expiry), and jti (a newly generated GUID).
///
/// The unique identifier of the user the token is issued for.
/// The username of the user, included as a claim.
/// The date and time at which the token expires.
/// The symmetric secret used to sign the token (encoded as UTF-8 bytes).
/// The serialized, signed JWT string.
public string GenerateJwt(
Guid userId,
string username,
DateTime expiry,
string secret
)
{
JsonWebTokenHandler handler = new();
byte[] key = Encoding.UTF8.GetBytes(secret);
List claims = new()
{
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
new Claim(JwtRegisteredClaimNames.UniqueName, username),
new Claim(
JwtRegisteredClaimNames.Iat,
DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()
),
new Claim(
JwtRegisteredClaimNames.Exp,
new DateTimeOffset(expiry).ToUnixTimeSeconds().ToString()
),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
SecurityTokenDescriptor tokenDescriptor = new()
{
Subject = new ClaimsIdentity(claims),
Expires = expiry,
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256
)
};
return handler.CreateToken(tokenDescriptor);
}
///
/// Validates a JWT's signature and lifetime (issuer and audience validation are disabled),
/// using the provided secret as the HMAC-SHA256 symmetric signing key.
///
/// The JWT string to validate.
/// The symmetric secret used to verify the token's signature (encoded as UTF-8 bytes).
/// A wrapping the validated token's claims identity.
///
/// Thrown when the token is invalid, has no claims identity, is expired, or otherwise fails validation
/// (including when validation itself throws, e.g. due to a malformed token or signature mismatch).
///
public async Task ValidateJwtAsync(
string token,
string secret
)
{
JsonWebTokenHandler handler = new();
byte[] keyBytes = Encoding.UTF8.GetBytes(
secret
);
TokenValidationParameters parameters = new()
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(keyBytes)
};
try
{
TokenValidationResult? result = await handler.ValidateTokenAsync(token, parameters);
if (!result.IsValid || result.ClaimsIdentity == null)
throw new UnauthorizedAccessException();
return new ClaimsPrincipal(result.ClaimsIdentity);
}
catch (Exception e)
{
throw new UnauthorizedException("Invalid token");
}
}
}