using QuestionsApi.Models; using System.Security.Cryptography; using System.Text; using System.Text.Json; namespace QuestionsApi.Services; public sealed class DynamicQuestionTokenService { private readonly byte[] _key; private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web); public DynamicQuestionTokenService(IConfiguration configuration) { var key = configuration["DynamicQuestions:SigningKey"] ?? configuration["Jwt:Key"] ?? "development-dynamic-question-signing-key-change-me"; _key = SHA256.HashData(Encoding.UTF8.GetBytes(key)); } public string SignGeneration(DynamicGenerationClaims claims) => Sign(claims); public string SignRun(DynamicRunClaims claims) => Sign(claims); public DynamicGenerationClaims ReadGeneration(string token, string userId) => Read(token, userId); public DynamicRunClaims ReadRun(string token, string userId) => Read(token, userId); private string Sign(T value) { var payload = Base64Url(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value, Json))); using var hmac = new HMACSHA256(_key); return $"{payload}.{Base64Url(hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)))}"; } private T Read(string token, string userId) where T : class { var parts = (token ?? "").Split('.'); if (parts.Length != 2) throw new ArgumentException("token is invalid"); using var hmac = new HMACSHA256(_key); var expected = hmac.ComputeHash(Encoding.UTF8.GetBytes(parts[0])); var supplied = FromBase64Url(parts[1]); if (!CryptographicOperations.FixedTimeEquals(expected, supplied)) throw new ArgumentException("token is invalid"); var value = JsonSerializer.Deserialize(FromBase64Url(parts[0]), Json) ?? throw new ArgumentException("token is invalid"); var tokenUser = value switch { DynamicGenerationClaims generation => generation.UserId, DynamicRunClaims run => run.UserId, _ => "" }; var expires = value switch { DynamicGenerationClaims generation => generation.ExpiresUtc, DynamicRunClaims run => run.ExpiresUtc, _ => DateTime.MinValue }; if (!string.Equals(tokenUser, userId, StringComparison.Ordinal) || expires < DateTime.UtcNow) throw new ArgumentException("token is expired or belongs to another user"); return value; } private static string Base64Url(byte[] value) => Convert.ToBase64String(value).TrimEnd('=').Replace('+', '-').Replace('/', '_'); private static byte[] FromBase64Url(string value) { value = value.Replace('-', '+').Replace('_', '/'); value += new string('=', (4 - value.Length % 4) % 4); return Convert.FromBase64String(value); } }