using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using DysonNetwork.Common.Models; using DysonNetwork.Pass.Data; using DysonNetwork.Pass.Features.Auth.Interfaces; using DysonNetwork.Pass.Features.Auth.Models; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using NodaTime; namespace DysonNetwork.Pass.Features.Auth.Services; public class AuthenticationService : IAuthenticationService { private readonly PassDatabase _db; private readonly IConfiguration _configuration; private readonly ISessionService _sessionService; private readonly IOidcService _oidcService; private readonly IHttpContextAccessor _httpContextAccessor; public AuthenticationService( PassDatabase db, IConfiguration configuration, ISessionService sessionService, IOidcService oidcService, IHttpContextAccessor httpContextAccessor) { _db = db; _configuration = configuration; _sessionService = sessionService; _oidcService = oidcService; _httpContextAccessor = httpContextAccessor; } public async Task AuthenticateAsync(string username, string password) { // First try to find by username (Name in the Account model) var account = await _db.Accounts .Include(a => a.Profile) // Include Profile for email lookup .FirstOrDefaultAsync(a => a.Name == username); // If not found by username, try to find by email in the Profile if (account == null) { account = await _db.Accounts .Include(a => a.Profile) .FirstOrDefaultAsync(a => a.Profile != null && a.Profile.Email == username); } if (account == null || !await VerifyPasswordAsync(account, password)) { return new AuthResult { Success = false, Error = "Invalid username/email or password" }; } return await CreateAuthResult(account); } private async Task VerifyPasswordAsync(Account account, string password) { // Find password auth factor for the account var passwordFactor = await _db.AccountAuthFactors .FirstOrDefaultAsync(f => f.AccountId == account.Id && f.FactorType == AccountAuthFactorType.Password); if (passwordFactor == null) return false; return BCrypt.Net.BCrypt.Verify(password, passwordFactor.Secret); } public async Task AuthenticateWithOidcAsync(string provider, string code, string state) { return await _oidcService.AuthenticateAsync(provider, code, state); } public async Task RefreshTokenAsync(string refreshToken) { var session = await _db.AuthSessions .FirstOrDefaultAsync(s => s.RefreshToken == refreshToken && !s.IsRevoked); if (session == null || session.RefreshTokenExpiryTime <= SystemClock.Instance.GetCurrentInstant()) { return new AuthResult { Success = false, Error = "Invalid or expired refresh token" }; } var account = await _db.Accounts.FindAsync(session.AccountId); if (account == null) { return new AuthResult { Success = false, Error = "Account not found" }; } // Invalidate the old session await _sessionService.InvalidateSessionAsync(session.Id); // Create a new session return await CreateAuthResult(account); } public async Task ValidateTokenAsync(string token) { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Key"]!); try { tokenHandler.ValidateToken(token, new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = true, ValidateAudience = false, ClockSkew = TimeSpan.Zero, ValidIssuer = _configuration["Jwt:Issuer"] }, out _); return true; } catch { return false; } } public async Task LogoutAsync(Guid sessionId) { await _sessionService.InvalidateSessionAsync(sessionId); } public async Task ValidateSessionAsync(Guid sessionId) { return await _sessionService.ValidateSessionAsync(sessionId); } public async Task GetSessionAsync(Guid sessionId) { var session = await _sessionService.GetSessionAsync(sessionId); if (session == null) throw new Exception("Session not found"); return session; } private async Task CreateAuthResult(Account account) { var ipAddress = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString() ?? string.Empty; var userAgent = _httpContextAccessor.HttpContext?.Request.Headers.UserAgent.ToString() ?? string.Empty; var session = await _sessionService.CreateSessionAsync(account.Id, ipAddress, userAgent); var token = GenerateJwtToken(account, session.Id); return new AuthResult { Success = true, AccessToken = token, RefreshToken = session.RefreshToken, Session = session }; } private string GenerateJwtToken(Account account, Guid sessionId) { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Key"]!); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, account.Id.ToString()), new Claim(ClaimTypes.Name, account.Username), new Claim("session_id", sessionId.ToString()) }), Expires = DateTime.UtcNow.AddDays(7), SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature ), Issuer = _configuration["Jwt:Issuer"], Audience = _configuration["Jwt:Audience"] }; var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } private bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt) { using var hmac = new HMACSHA512(storedSalt); var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password)); return computedHash.SequenceEqual(storedHash); } }