using System.Diagnostics; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using adas_core.Application.Exceptions; using adas_core.Application.Repositories.Interfaces; using adas_core.Application.Services.Interfaces; using adas_core.Authentication.Interfaces; using adas_core.Authentication.Models; using adas_core.Domain.Enums; using adas_core.Domain.Exceptions; using adas_core.Domain.Models.AppSettings; using adas_core.Domain.Models.DTO; using adas_core.Domain.Models.Filter; using adas_core.Domain.Models.MongoModels; using adas_core.Domain.Models.Responses; using adas_core.Domain.Utils; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using MongoDB.Bson; using MongoDB.Driver; using static System.Enum; namespace adas_core.Authentication; public class UserService : IUserService { private readonly ILocalAuditService _auditService; private readonly IAuthorityService _authorityService; private readonly IEnumerable _availableLoginServices; private readonly Lazy _displayService; private readonly IHttpContextAccessor _httpContextAccessor; private readonly JwtConfig _jwtConfig; private readonly ILogger _logger; private readonly IUserRepository _userRepository; private readonly UsersWhiteListConfig _usersWhiteList; private readonly ValidGroupsConfig _validGroups; private readonly List _validLoginMethods; private readonly ISubscribersService _subscribersService; private readonly IClientMessageService _clientMessageService; private readonly Lazy _permissionService; public UserService( IEnumerable loginServices, IOptions configuration, IOptions validGroups, IOptions usersWhiteList, IOptions jwt, IHttpContextAccessor httpContextAccessor, IUserRepository userRepository, IAuthorityService authorityService, ILocalAuditService auditService, Lazy displayService, ILogger logger, ISubscribersService subscribersService, IClientMessageService clientMessageService, Lazy permissionService) { _availableLoginServices = loginServices; _httpContextAccessor = httpContextAccessor; _validGroups = validGroups.Value; _usersWhiteList = usersWhiteList.Value.UsersWhiteListConfig; _jwtConfig = jwt.Value.JwtConfig ?? throw new Exception("JWT NOT CONFIGURED"); var loginMethods = configuration.Value.LoginMethods; _validLoginMethods = loginMethods; _userRepository = userRepository; _authorityService = authorityService; _auditService = auditService; _displayService = displayService; _logger = logger; _subscribersService = subscribersService; _clientMessageService = clientMessageService; _permissionService = permissionService; } public async Task Login(string username, string password) { var user = await GetUser(username, password); _logger.LogInformation("[UserService] Login user {u}", user.ToString()); if (user == null) throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedInvalidCredentials); if (!user.IsEnabled || (user.LockExpirationDate != null && user.LockExpirationDate > DateTime.UtcNow)) throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenUserBlocked); if (!_usersWhiteList.IsValid(user) && !_validGroups.IsValid(user)) throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedNoPermission); if (user.LockExpirationDate < DateTime.UtcNow) user.LockExpirationDate = null; user.LastLogin = DateTime.UtcNow; await UpdateUsersByRequest(user, false); return await GenerateJwt(user); } public void ValidateToken(string token, string tokenType, out SecurityToken validatedToken) { try { var tokenValidationParameters = new TokenValidationParameters { ValidateAudience = _jwtConfig.ValidateAudience, ValidateIssuer = _jwtConfig.ValidateIssuer, ValidAudience = _jwtConfig.Audience, ValidIssuer = _jwtConfig.Issuer, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtConfig.Key ?? string.Empty)), ValidateLifetime = false }; var tokenHandler = new JwtSecurityTokenHandler(); tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken); if (securityToken is not JwtSecurityToken jwtSecurityToken || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) throw new SecurityTokenException("Invalid token"); validatedToken = securityToken; } catch (SecurityTokenException a) { _logger.LogError("Error validating token, {msg}", a.Message); throw new TokenException(a.Message); } catch (Exception e) { _logger.LogError("Error validating token, {msg}", e.Message); throw new TokenException("Error validating token"); } } public async Task GetUserByToken(JwtSecurityToken? jwtToken) { User? user = null; if (jwtToken == null) return user; var userName = jwtToken.Claims.First(x => x.Type == ClaimTypes.Name).Value; user = await _userRepository.GetByUserName(userName); if (user == null) return user; //Eliminamos la contraseña user.Password = string.Empty; if (user.Authorization == null || user.Authorization.Count == 0) user.Authorization = await _authorityService.GetUserAuthorities(user.Id); return user; } public async Task GetUserByCasTicket(string serviceUrl, string ticket) { var service = _availableLoginServices.FirstOrDefault(s => s.Method == UserEnum.LoginMethod.Cas); if (service == null) throw new LoginServicesException("CAS login method not available"); try { var user = await service.Login(_httpContextAccessor.HttpContext!); if (!_usersWhiteList.IsValid(user) && !_validGroups.IsValid(user)) throw new UnauthorizedAccessException("You don't have sufficient permissions"); return user; } catch (BusinessException) { throw; } catch (UnauthorizedAccessException) { throw; } catch (Exception e) { var message = e.Message; if (e.InnerException != null && e.Message != e.InnerException?.Message) message += $", {e.InnerException?.Message}"; Debug.WriteLine($"Error while login: {message} "); throw new LoginServicesException(message, e); } //////////////////////// /* if (!_validLoginMethods.Contains(LoginMethod.CAS.ToString())) { Log.Warning("CAS login method not enabled"); return null; } _casLoginService = new Log.Information("CAS ticket received: '{CasTicket}'", ticket); return string.IsNullOrEmpty(ticket) ? null : _casLoginService.GetUser(serviceUrl, ticket, context); */ } public async Task RefreshToken(string refreshToken) { ValidateToken(refreshToken, IUserService.TokenTypeRefresh, out var validatedToken); var user = await GetUserByToken((JwtSecurityToken)validatedToken); if (user == null) return new TokenResult(); return await GenerateJwt(user); } public async Task GetUser(string username, string password) { //BusinessException? loginException = null; foreach (var m in _validLoginMethods) { if (!TryParse(m, out UserEnum.LoginMethod method)) continue; var service = _availableLoginServices.FirstOrDefault(s => s.Method == method && s.AllowPassword); if (service == null) continue; try { _logger.LogInformation("[UserService] Trying login with method {method}", method); return await service.Login(username, password); } catch (Exception e) { _logger.LogError("[UserService] Error while trying method {method}, err {err}", method, e.Message); } } throw new LoginServicesNotFoundException(); } public async Task GetUserById(ObjectId id) { try { var u = await _userRepository.GetById(id); if (u != null) u.Authorization = await _authorityService.GetUserAuthorities(u.Id); return u; } catch (Exception) { return null; } } public async Task GetUserByUserName(string name) { var u = await _userRepository.GetByUserName(name); if (u == null) return u; u.Authorization = await _authorityService.GetUserAuthorities(u.Id); return u; } public async Task GetUserByName(string name) { try { var u = await _userRepository.GetByName(name); if (u != null) u.Authorization = await _authorityService.GetUserAuthorities(u.Id); return u; } catch (Exception) { return null; } } public async Task CreateUser(User userEntryLdap) { CheckIfUserNameExists(userEntryLdap.UserName); CheckIfEmailExists(userEntryLdap.Email); await _userRepository.InsertOneAsync(userEntryLdap); var result = await GetUserByUserName(userEntryLdap.UserName); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, null, result); return result; } public async Task CreateNewUserByRequest(User user) { CheckIfEmailExists(user.Email); CheckIfUserNameExists(user.UserName); if (user.Type == UserEnum.Type.Local) { if (!CryptoAdas.IsStrongPassword(user.Password)) throw new UnprocessableEntityException(HttpEnum.ErrorMessage.UnprocessableEntityInvalidPassword); user.Password = CryptoAdas.CreateBCrypt(user.Password); } await _userRepository.InsertOneAsync(user); var result = await GetUserByUserName(user.UserName) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, null, result); return result; } public async Task CreateNewUserWithAuthorities(CreateUserWithAuthDto createUserWithAuthDto) { var user = createUserWithAuthDto.User; var newUser = await CreateNewUserByRequest(user) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); var auths = createUserWithAuthDto.Authorizations; foreach (var auth in auths) { auth.UserId = newUser.Id; await CreateNewAuthority(auth); } return newUser; } public async Task UpdateUsersByRequest(User user, bool updatePass) { var oldUser = await _userRepository.GetByUserName(user.UserName); var logoutHasBeenSent = false; if (updatePass && !CryptoAdas.IsStrongPassword(user.Password)) throw new UnprocessableEntityException(HttpEnum.ErrorMessage.UnprocessableEntityInvalidPassword); if (updatePass) { user.Password = CryptoAdas.CreateBCrypt(user.Password); await SendBroadcastUser(user, user.UserName, OperationType.UpdatePassword); logoutHasBeenSent = true; } var result = await _userRepository.UpdateUser(user, updatePass); if(!logoutHasBeenSent && oldUser != null && result != null && oldUser.IsEnabled && !result.IsEnabled) { await SendBroadcastUser(result, user.UserName, OperationType.DisableUser); logoutHasBeenSent = true; } if(!logoutHasBeenSent && result != null && result.LockExpirationDate != null && result.LockExpirationDate>DateTime.UtcNow) await SendBroadcastUser(result, user.UserName, OperationType.LockUser); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, oldUser, result); return result; } private async Task SendBroadcastUser(object elementUpdated, string userName, OperationType operation) { var subscribers = _subscribersService.GetSubscribers().Where(s => s.UserName == userName).ToList(); foreach (var subscriber in subscribers) { await _clientMessageService.SendAsync(subscriber.Id, operation, elementUpdated); } } private async Task SendBroadcastPermissions(object elementUpdated, string userName, OperationType operation) { var subscribers = _subscribersService.GetSubscribers().Where(s => s.UserName == userName).ToList(); foreach (var subscriber in subscribers) { if (subscriber.SubscriptionType == SubscriptionEnum.WsType.Admin) { try { var permissionsForPanel = await _permissionService.Value.GetPermissionsForPanel(userName); _ = _clientMessageService.SendAsync(subscriber.Id, operation, permissionsForPanel); } catch (Exception) { _ = _clientMessageService.SendAsync(subscriber.Id, operation, null); } } else { _ = _clientMessageService.SendAsync(subscriber.Id, operation, elementUpdated); } } } public async Task UpdateUserWithAuthorities(UpdateUserWithAuthDto updateUserWithAuthDto, bool updatePass) { var u = updateUserWithAuthDto.User; CheckIfEmailExists(u.Email, u.Id); CheckIfUserNameExists(u.UserName, u.Id); var userUpdated = await UpdateUsersByRequest(u, updatePass) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); foreach (var id in updateUserWithAuthDto.AuthsToDelete) await DeleteAuthority(id); foreach (var auth in updateUserWithAuthDto.Authorizations) await UpdateAuthority(auth); foreach (var auth in updateUserWithAuthDto.NewAuthorizations) { auth.UserId = userUpdated.Id; await CreateNewAuthority(auth); } if(updateUserWithAuthDto.AuthsToDelete.Any() || updateUserWithAuthDto.NewAuthorizations.Any() || updateUserWithAuthDto.Authorizations.Any()) { var allPermissionsForDisplay = await _displayService.Value.GetAllByUser(u.UserName); await SendBroadcastPermissions(allPermissionsForDisplay, u.UserName, OperationType.UpdateAuthorities); } return userUpdated; } public async Task UpdateUserPassword(ObjectId id, string oldPassword, string newPassword) { if (oldPassword == newPassword) throw new BadRequestException("New password must be different"); var user = await _userRepository.GetById(id) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); if (!CryptoAdas.VerifyPassword(oldPassword, user.Password)) throw new UnprocessableEntityException("Invalid current password"); if (!CryptoAdas.IsStrongPassword(newPassword)) throw new UnprocessableEntityException("Weak password"); user.Password = CryptoAdas.CreateBCrypt(newPassword); var result = await _userRepository.UpdateUser(user, true); if (result != null) { _logger.LogInformation("Password updated for user {UserId}", id); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, user, result); return true; } return false; } public async Task DeleteUser(ObjectId id) { try { var user = await _userRepository.GetById(id); await _userRepository.DeleteAsync(id); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, user, null); return true; } catch (Exception e) { _logger.LogError("Error deleting user. Exception: {e}", e); return false; } } public async Task> GetPaginatedUsers(PaginationFilter filter) { var result = _userRepository.GetPaginatedUsers(filter); var count = await result.CountDocumentsAsync(); var data = await result.Skip((filter.PageNumber - 1) * filter.PageSize) .Limit(filter.PageSize) .ToCursorAsync(); var dataList = await data.ToListAsync(); return new PaginationResponse(dataList, filter.PageNumber, filter.PageSize, count); } public async Task GenerateJwt(User user) { var claims = new List { new(ClaimTypes.Name, user.UserName), new(ClaimTypes.Email, user.Email), new("IpAddress", user.IpAddress) }; // Agregar claims de autorizaciones utilizando ClaimsFromAuthorities if (user.Authorization != null) claims.AddRange(await ClaimsFromAuthorities(user.Authorization)); var accessToken = JwtHelper.GetJwtToken( user.UserName, _jwtConfig.Key ?? string.Empty, _jwtConfig.Issuer ?? string.Empty, _jwtConfig.Audience ?? string.Empty, _jwtConfig.TokenValidityInMinutes, claims.ToArray()); var refreshToken = JwtHelper.GetJwtToken( user.UserName, _jwtConfig.Key ?? string.Empty, _jwtConfig.Issuer ?? string.Empty, _jwtConfig.Audience ?? string.Empty, _jwtConfig.RefreshTokenValidityInMinutes, claims.ToArray()); return new TokenResult { AccessToken = new JwtSecurityTokenHandler().WriteToken(accessToken), RefreshToken = new JwtSecurityTokenHandler().WriteToken(refreshToken), AccessTokenExpiryTime = accessToken.ValidTo, RefreshTokenExpiryTime = refreshToken.ValidTo, User = user }; } public async Task> GetAll() { BusinessException? loginException = null; foreach (var m in _validLoginMethods) { if (!TryParse(m, out UserEnum.LoginMethod method)) continue; var service = _availableLoginServices.FirstOrDefault(s => s.Method == method && s.AllowPassword); if (service == null) continue; try { return await service.GetAllUsers(); } catch (BusinessException e) { loginException = e; } catch (Exception e) { var message = e.Message; if (e.Message != e.InnerException?.Message) message += $", {e.InnerException?.Message}"; Debug.WriteLine($"Error Getting all users: {message} "); loginException = new LoginServicesException(message); } } if (loginException != null) throw loginException; throw new LoginServicesNotFoundException(); } public async Task LoginWithGivenAccessToken(string token) { ValidateToken(token, IUserService.TokenTypeUser, out var validatedToken); var jwtToken = (JwtSecurityToken)validatedToken; var user = await GetUserByToken(jwtToken) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); user.Authorization ??= await _authorityService.GetUserAuthorities(user.Id); if (jwtToken.ValidTo < DateTime.UtcNow) return await GenerateJwt(user); var claims = new List { new(ClaimTypes.Name, user.UserName), new(ClaimTypes.Email, user.Email), new("IpAddress", user.IpAddress) }; // Agregar claims de autorizaciones utilizando ClaimsFromAuthorities claims.AddRange(await ClaimsFromAuthorities(user.Authorization)); var dif = jwtToken.ValidTo > DateTime.Now ? jwtToken.ValidTo - DateTime.Now : DateTime.Now - jwtToken.ValidTo; var accessToken = JwtHelper.GetJwtToken( user.UserName, _jwtConfig.Key ?? string.Empty, _jwtConfig.Issuer ?? string.Empty, _jwtConfig.Audience ?? string.Empty, (int)dif.TotalMinutes, claims.ToArray()); var refreshToken = JwtHelper.GetJwtToken( user.UserName, _jwtConfig.Key ?? string.Empty, _jwtConfig.Issuer ?? string.Empty, _jwtConfig.Audience ?? string.Empty, (int)dif.TotalMinutes + _jwtConfig.RefreshTokenValidityInMinutes, claims.ToArray()); return new TokenResult { AccessToken = new JwtSecurityTokenHandler().WriteToken(accessToken), RefreshToken = new JwtSecurityTokenHandler().WriteToken(refreshToken), AccessTokenExpiryTime = accessToken.ValidTo, RefreshTokenExpiryTime = refreshToken.ValidTo, User = user }; } public async Task LoginWithAccessToken(string token) { ValidateToken(token, IUserService.TokenTypeUser, out var validatedToken); var jwtToken = (JwtSecurityToken)validatedToken; var user = await GetUserByToken(jwtToken) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); user.Authorization ??= await _authorityService.GetUserAuthorities(user.Id); return await GenerateJwt(user); } public async Task CreateNewAuthority(Authorization auth) { var newUserAuthority = await _authorityService.CreateNewUserAuthority(auth) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed); return newUserAuthority; } public async Task DeleteAuthority(string id) { if (!ObjectId.TryParse(id, out var userAuthorityIdParsed)) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); var isDeleted = await _authorityService.DeleteUserAuthority(userAuthorityIdParsed); return !isDeleted ? throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed) : isDeleted; } public async Task UpdateAuthority(Authorization authorization) { _ = await _authorityService.EditUserAuthority(authorization) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); return true; } public async Task> ClaimsFromAuthorities(List authorities) { var claims = new List(); foreach (var auth in authorities) if (auth.UnitId != null) { var dis = await _displayService.Value.GetByUnitId(ObjectId.Parse(auth.UnitId)); foreach (var claim in dis) { auth.DisplayId = claim.Id.ToString(); AddClaim(auth, claims); } } else { AddClaim(auth, claims); } return claims; } private static void AddClaim(Authorization auth, List claims) { var newClaim = new Claim(ClaimTypes.Role, auth.Rol + (string.IsNullOrEmpty(auth.DisplayId) ? "" : "_" + auth.DisplayId)); if (claims.Any(c => c.Value == newClaim.Value)) return; //newClaim.Properties.Add("DisplayId", auth.DisplayId ?? ""); claims.Add(newClaim); } private void CheckIfUserNameExists(string userName, ObjectId? userId = null) { var users = GetAll().Result; var userNameExists = userId.HasValue ? users.Any(user => user.Id != userId.Value && user.UserName == userName) : users.Any(user => user.UserName == userName); if (userNameExists) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectEmailOrUsername); } private void CheckIfEmailExists(string email, ObjectId? userId = null) { var users = GetAll().Result; var emailExists = userId.HasValue ? users.Any(user => user.Id != userId.Value && user.Email == email) : users.Any(user => user.Email == email); if (emailExists && !string.IsNullOrEmpty(email)) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectEmailOrUsername); } }