using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Microsoft.IdentityModel.Tokens; namespace SatelliteProvider.Tests.TestUtilities; public static class JwtTokenFactory { public const string DefaultSubject = "test-user"; public static string Create( string secret, string subject = DefaultSubject, TimeSpan? lifetime = null, IEnumerable? extraClaims = null, string algorithm = SecurityAlgorithms.HmacSha256) { ArgumentNullException.ThrowIfNull(secret); var keyBytes = Encoding.UTF8.GetBytes(secret); var signingKey = new SymmetricSecurityKey(keyBytes); var credentials = new SigningCredentials(signingKey, algorithm); var now = DateTime.UtcNow; var expires = now.Add(lifetime ?? TimeSpan.FromHours(1)); // JwtSecurityToken rejects Expires <= NotBefore. For negative // lifetimes (expired-token test fixture) shift NotBefore behind // Expires so the constructor accepts the token and lifetime // validation can fire downstream. var notBefore = expires <= now ? expires.AddMinutes(-5) : now; var claims = new List { new(JwtRegisteredClaimNames.Sub, subject), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")) }; if (extraClaims is not null) { claims.AddRange(extraClaims); } var token = new JwtSecurityToken( issuer: null, audience: null, claims: claims, notBefore: notBefore, expires: expires, signingCredentials: credentials); return new JwtSecurityTokenHandler().WriteToken(token); } public static string CreateExpired(string secret, string subject = DefaultSubject) { return Create(secret, subject, lifetime: TimeSpan.FromMinutes(-5)); } public static string TamperSignature(string token) { ArgumentException.ThrowIfNullOrEmpty(token); var parts = token.Split('.'); if (parts.Length != 3) { throw new ArgumentException("JWT must have three dot-separated parts.", nameof(token)); } var signature = parts[2]; if (signature.Length == 0) { throw new ArgumentException("JWT signature segment is empty.", nameof(token)); } var firstChar = signature[0]; var replacement = firstChar == 'A' ? 'B' : 'A'; parts[2] = replacement + signature[1..]; return string.Join('.', parts); } }