using System.Security.Claims; using System.Text; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames; using Domain.Exceptions; 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 ) { var handler = new JsonWebTokenHandler(); var key = Encoding.UTF8.GetBytes(secret); var claims = new List { new(JwtRegisteredClaimNames.Sub, userId.ToString()), new(JwtRegisteredClaimNames.UniqueName, username), new( JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString() ), new( JwtRegisteredClaimNames.Exp, new DateTimeOffset(expiry).ToUnixTimeSeconds().ToString() ), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; var tokenDescriptor = new SecurityTokenDescriptor { 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 ) { var handler = new JsonWebTokenHandler(); var keyBytes = Encoding.UTF8.GetBytes( secret ); var parameters = new TokenValidationParameters { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, IssuerSigningKey = new SymmetricSecurityKey(keyBytes), }; try { var 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"); } } }