using System; using System.Collections.Generic; using System.DirectoryServices.AccountManagement; using System.IdentityModel.Tokens.Jwt; using System.Security.Authentication; using System.Security.Claims; using System.Security.Cryptography; using System.Security.Principal; using System.Text; using System.Threading.Tasks; using Fab2ApprovalSystem.Models; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; namespace Fab2ApprovalSystem.Services; public interface IAuthenticationService { public Task AuthenticateUser(AuthAttempt login); public Task AttemptLocalUserAuth(WindowsIdentity identity); public AuthTokens GenerateAuthTokens(AuthAttempt authAttempt, IEnumerable roles); public Task RefreshAuthTokens(AuthAttempt authAttempt); } public class AuthenticationService : IAuthenticationService { private readonly ILogger _logger; private readonly IMemoryCache _cache; private readonly IUserService _userService; private readonly string? _jwtIssuer; private readonly string? _jwtAudience; private readonly string? _jwtKey; public AuthenticationService(ILogger logger, IMemoryCache cache, IUserService userService, AppSettings appSettings) { _logger = logger ?? throw new ArgumentNullException("ILogger not injected"); _cache = cache ?? throw new ArgumentNullException("IMemoryCache not injected"); _userService = userService ?? throw new ArgumentNullException("IUserService not injected"); _jwtKey = appSettings.JwtKey; _jwtIssuer = appSettings.JwtIssuer; _jwtAudience = appSettings.JwtAudience; } public async Task AuthenticateUser(AuthAttempt login) { try { _logger.LogInformation("Attempting to authenticate user"); if (login is null) throw new ArgumentNullException("Login cannot be null"); string domain = "infineon.com"; using (PrincipalContext pc = new(ContextType.Domain, domain)) { bool isValid = pc.ValidateCredentials(login.LoginID, login.Password); if (isValid) { User? user = _cache.Get($"user{login.LoginID}"); if (user is null) { user = await _userService.GetUserByLoginId(login.LoginID); _cache.Set($"user{login.LoginID}", user, DateTimeOffset.Now.AddDays(1)); } List roles = new(); if (user.IsManager) roles.Add("manager"); if (user.IsAdmin) roles.Add("admin"); AuthTokens tokens = GenerateAuthTokens(login, roles); return new LoginResult { IsAuthenticated = true, AuthTokens = tokens, User = user }; } else { return new LoginResult() { IsAuthenticated = false, AuthTokens = new() { JwtToken = "", RefreshToken = "" }, User = null }; } } } catch (Exception ex) { _logger.LogError($"An exception occurred when attempting to authenticate user. Exception: {ex.Message}"); throw; } } public async Task AttemptLocalUserAuth(WindowsIdentity identity) { try { _logger.LogInformation("Attempting to authenticate local Windows system user"); if (identity is null) throw new ArgumentNullException("WindowsIdentity cannot be null"); User user = await _userService.GetUserByLoginId(identity.Name); List roles = new(); if (user.IsManager) roles.Add("manager"); if (user.IsAdmin) roles.Add("admin"); AuthAttempt authAttempt = new() { LoginID = user.LoginID, }; AuthTokens tokens = GenerateAuthTokens(authAttempt, roles); return new LoginResult { IsAuthenticated = true, AuthTokens = tokens, User = user }; } catch (Exception ex) { _logger.LogError($"Unable to authenticate local Windows system user, because {ex.Message}"); throw; } } public AuthTokens GenerateAuthTokens(AuthAttempt authAttempt, IEnumerable roles) { try { _logger.LogInformation("Attempting to generate JWT"); if (authAttempt is null) throw new ArgumentNullException("AuthAttempt cannot be null"); if (string.IsNullOrWhiteSpace(authAttempt.LoginID)) throw new ArgumentException("UserName cannot be null or empty"); if (roles is null) throw new ArgumentNullException("roles cannot be null"); byte[] key = Encoding.ASCII.GetBytes(_jwtKey); List claims = new() { new Claim(nameof(authAttempt.LoginID), authAttempt.LoginID) }; foreach (string role in roles) { claims.Add(new Claim(ClaimTypes.Role, role)); } ClaimsIdentity identity = new(claims); SecurityTokenDescriptor tokenDescriptor = new() { Issuer = _jwtIssuer, Audience = _jwtAudience, Subject = identity, NotBefore = DateTime.Now, Expires = DateTime.Now.AddHours(8), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; JwtSecurityTokenHandler tokenHandler = new(); JwtSecurityToken token = tokenHandler.CreateJwtSecurityToken(tokenDescriptor); string jwt = tokenHandler.WriteToken(token); string refreshToken = GenerateRefreshToken(); List? refreshTokensForUser = _cache.Get>(authAttempt.LoginID); refreshTokensForUser ??= new List(); if (refreshTokensForUser.Count > 9) refreshTokensForUser.RemoveRange(9, refreshTokensForUser.Count - 9); refreshTokensForUser.Insert(0, refreshToken); _cache.Set(authAttempt.LoginID, refreshTokensForUser, DateTimeOffset.Now.AddHours(4)); return new AuthTokens { JwtToken = jwt, RefreshToken = refreshToken }; } catch (Exception ex) { _logger.LogError($"An exception occurred when attempting to generate JWT. Exception: {ex.Message}"); throw; } } public async Task RefreshAuthTokens(AuthAttempt authAttempt) { try { _logger.LogInformation("Attempting to refresh auth tokens"); if (authAttempt is null) throw new ArgumentNullException("AuthAttempt cannot be null"); if (authAttempt.AuthTokens is null) throw new ArgumentNullException("AuthTokens cannot be null"); bool refreshTokenIsValid = IsRefreshTokenValid(authAttempt.LoginID, authAttempt.AuthTokens.RefreshToken); if (refreshTokenIsValid) { User? user = _cache.Get($"user{authAttempt.LoginID}"); if (user is null) { user = await _userService.GetUserByLoginId(authAttempt.LoginID); _cache.Set($"user{authAttempt.LoginID}", user, DateTimeOffset.Now.AddDays(1)); } List roles = new(); if (user.IsManager) roles.Add("manager"); if (user.IsAdmin) roles.Add("admin"); AuthTokens refreshedTokens = GenerateAuthTokens(authAttempt, roles); LoginResult loginResult = new() { IsAuthenticated = true, AuthTokens = refreshedTokens, User = user }; return loginResult; } else { throw new AuthenticationException("Invalid refresh token"); } } catch (Exception ex) { _logger.LogError($"An exception occurred when attempting to refresh auth tokens. Exception: {ex.Message}"); throw; } } private string GenerateRefreshToken() { byte[] randomNumber = new byte[32]; using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) { rng.GetBytes(randomNumber); return Convert.ToBase64String(randomNumber); } } private bool IsRefreshTokenValid(string loginId, string refreshToken) { try { _logger.LogInformation("Attempting to determine if refresh token is valid"); if (string.IsNullOrWhiteSpace(loginId)) throw new ArgumentNullException("LoginID cannot be null or empty"); if (string.IsNullOrWhiteSpace(refreshToken)) throw new ArgumentNullException("Refresh token cannot be null or empty"); List? cachedRefreshTokensForUser = _cache.Get>(loginId); if (cachedRefreshTokensForUser is null || !cachedRefreshTokensForUser.Contains(refreshToken)) { _logger.LogInformation($"Could not find cached refresh tokens for user {loginId}"); return false; } return true; } catch (Exception ex) { _logger.LogError($"An exception occurred when attempting to validate refresh token. Exception: {ex.Message}"); throw; } } }