using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using OTSSignsOrchestrator.Server.Data; using OTSSignsOrchestrator.Server.Data.Entities; namespace OTSSignsOrchestrator.Server.Auth; public class OperatorAuthService { private readonly OrchestratorDbContext _db; private readonly JwtOptions _jwt; private readonly ILogger _logger; public OperatorAuthService( OrchestratorDbContext db, IOptions jwt, ILogger logger) { _db = db; _jwt = jwt.Value; _logger = logger; } public async Task<(string Jwt, string RefreshToken)> LoginAsync(string email, string password) { var op = await _db.Operators.FirstOrDefaultAsync( o => o.Email == email.Trim().ToLowerInvariant()); if (op is null || !BCrypt.Net.BCrypt.Verify(password, op.PasswordHash)) { _logger.LogWarning("Login failed for {Email}", email); throw new UnauthorizedAccessException("Invalid email or password."); } _logger.LogInformation("Operator {Email} logged in", op.Email); var jwt = GenerateJwt(op); var refresh = await CreateRefreshTokenAsync(op.Id); return (jwt, refresh); } public async Task RefreshAsync(string refreshToken) { var token = await _db.RefreshTokens .Include(r => r.Operator) .FirstOrDefaultAsync(r => r.Token == refreshToken); if (token is null || token.RevokedAt is not null || token.ExpiresAt < DateTime.UtcNow) throw new UnauthorizedAccessException("Invalid or expired refresh token."); // Revoke the used token (single-use rotation) token.RevokedAt = DateTime.UtcNow; await _db.SaveChangesAsync(); _logger.LogInformation("Refresh token used for operator {Email}", token.Operator.Email); return GenerateJwt(token.Operator); } private string GenerateJwt(Operator op) { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwt.Key)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claims = new[] { new Claim(JwtRegisteredClaimNames.Sub, op.Id.ToString()), new Claim(JwtRegisteredClaimNames.Email, op.Email), new Claim(ClaimTypes.Name, op.Email), new Claim(ClaimTypes.Role, op.Role.ToString()), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; var token = new JwtSecurityToken( issuer: _jwt.Issuer, audience: _jwt.Audience, claims: claims, expires: DateTime.UtcNow.AddMinutes(15), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); } private async Task CreateRefreshTokenAsync(Guid operatorId) { var tokenValue = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); _db.RefreshTokens.Add(new RefreshToken { Id = Guid.NewGuid(), OperatorId = operatorId, Token = tokenValue, ExpiresAt = DateTime.UtcNow.AddDays(7), }); await _db.SaveChangesAsync(); return tokenValue; } }