using Microsoft.IdentityModel.Tokens; using MongoDB.Driver; using QuestionsApi.Models; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; namespace QuestionsApi.Services; public sealed class TokenBlacklistStore(MongoDatabaseProvider mongo) { private readonly IMongoCollection _tokens = mongo.Database.GetCollection("BlacklistedTokens"); public Task AddAsync(string jti, DateTime expiresAt) => _tokens.ReplaceOneAsync(token => token.Jti == jti, new BlacklistedToken { Jti = jti, ExpiresAt = expiresAt }, new ReplaceOptions { IsUpsert = true }); public Task ContainsAsync(string jti) => _tokens.Find(token => token.Jti == jti).AnyAsync(); internal IMongoCollection Collection => _tokens; } public sealed class JwtTokenService(IConfiguration configuration) { public (string Token, string Jti, DateTime ExpiresUtc) Generate(AuthUser user) { var key = Encoding.UTF8.GetBytes(configuration["Jwt:Key"] ?? throw new InvalidOperationException("Jwt:Key missing")); var issuer = configuration["Jwt:Issuer"] ?? "promiscuity"; var audience = configuration["Jwt:Audience"] ?? issuer; var credentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256); var jti = Guid.NewGuid().ToString("N"); var expiresUtc = DateTime.UtcNow.AddMinutes(15); var claims = new[] { new Claim(ClaimTypes.Name, user.Username), new Claim(ClaimTypes.NameIdentifier, user.Id), new Claim(ClaimTypes.Role, user.Role), new Claim(JwtRegisteredClaimNames.Jti, jti), new Claim("auth_ver", user.AuthVersion.ToString(System.Globalization.CultureInfo.InvariantCulture)) }; var token = new JwtSecurityToken(issuer, audience, claims, expires: expiresUtc, signingCredentials: credentials); return (new JwtSecurityTokenHandler().WriteToken(token), jti, expiresUtc); } public static string NewRefreshToken() => Convert.ToHexString(RandomNumberGenerator.GetBytes(32)).ToLowerInvariant(); }