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; /// /// Provides the concrete implementation of user-related service operations defined by the contract. /// /// /// This class is the default service component responsible for handling user domain logic and delegating persistence or infrastructure concerns as required by the interface. /// 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; } /// /// Authenticates a user with the provided credentials and generates a JWT token upon successful authentication. /// Performs validations for user existence, account status (enabled and not locked), and authorization via whitelist or group membership. /// Updates the user's last login timestamp and clears the lock expiration if it has expired before issuing the token. /// /// The username used to look up the user account. /// The password used to verify the user's credentials. /// A containing the generated for the authenticated user. /// Thrown when no user matches the provided credentials, or when the user is not in the whitelist or valid groups. /// Thrown when the user account is disabled or currently locked. 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); } /// /// Validates a JWT token using the configured audience, issuer, and symmetric signing key, and returns the validated security token. /// The token must be a JWT signed with the HmacSha256 algorithm; otherwise, validation fails and a is thrown. /// /// The JWT token string to validate. /// The type of the token being validated. /// When the method returns, contains the validated if validation succeeds. /// Thrown when the token is invalid, is not signed with the HmacSha256 algorithm, or any other validation error occurs. 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; } /// /// Authenticates a user using a CAS (Central Authentication Service) ticket and returns the associated user if authorization succeeds. /// /// The URL of the service requesting CAS authentication. /// The CAS ticket issued by the CAS server to validate. /// The authenticated if the ticket is valid and the user passes whitelist and group checks; otherwise, an exception is thrown. /// Thrown when the CAS login method is not available, or when an unexpected error occurs during authentication. /// Thrown when the authenticated user is not in the users whitelist and does not belong to any valid group. /// Rethrown when a business-level error occurs during the login process. 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); */ } /// /// Refreshes a JWT token using the provided refresh token. Validates the refresh token, retrieves the associated user, and generates a new JWT; if the user is not found, returns an empty TokenResult. /// /// The refresh token used to generate a new JWT. /// A TokenResult containing the new JWT, or an empty result if the user associated with the token is not found. 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); } /// /// Authenticates a user by iterating through the configured valid login methods, attempting each /// available password-enabled service until one succeeds. If every login attempt fails, a /// is thrown. /// /// The username to authenticate. /// The password to authenticate against. /// The authenticated returned by the first successful login service. /// Thrown when no configured login service successfully authenticates the 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(); } /// /// Retrieves a user by their unique identifier, including the user's assigned authorities when the user is found. /// Returns null if the user does not exist or if an error occurs while loading the user or its authorities. /// /// The unique identifier of the user to retrieve. /// A containing the user with its authorization populated, or null if the user cannot be found or an exception is thrown during retrieval. 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; } } /// /// Retrieves a user by their username, including their assigned authorities. Returns null if no user with the specified name exists. /// /// The username to look up. /// The matching with its Authorization property populated from the authority service, or null if no user is found. 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; } /// /// Retrieves a user by their name, including the user's authorization information if the user is found. /// Returns null if the user does not exist or if an error occurs while retrieving the data. /// /// The name of the user to look up. /// A instance with its Authorization populated when found; otherwise, null. 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; } } /// /// Creates a new user from LDAP data after validating that the username and email are not already in use, persists the entry, and records the creation in the audit log. /// /// The user information sourced from LDAP to be created in the system. /// The created user retrieved by username, or null if the user cannot be found after insertion. 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; } /// /// Creates a new user after validating email and username uniqueness; for local accounts, it enforces password strength and hashes the password before persistence. /// /// The user to create, including email, username, password, and account type. /// The created user retrieved by username after insertion. /// Thrown when a local user's password is not considered strong. /// Thrown when the user cannot be retrieved by username after insertion. 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; } /// /// Creates a new user along with the specified authorizations, associating each authorization with the newly created user. /// Throws an exception if the user cannot be created. /// /// The data transfer object containing the user details and the list of authorizations to associate with the new user. /// The newly created user, or null if creation fails (in which case an exception is thrown instead). /// Thrown when the user cannot be created by the underlying request, indicating a missing resource. 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; } /// /// Updates a user, optionally updating their password, and broadcasts relevant notifications when the password changes, the user is disabled, or the account becomes locked. /// /// The user with the updated information to be persisted. /// Indicates whether the user's password should be updated and re-hashed. /// The updated user returned by the repository, or null if the user could not be found or updated. /// Thrown when is true and the provided password does not meet the strong password policy. 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; } /// /// Sends a broadcast message asynchronously to all subscribers that match the specified user name, notifying them of an update operation performed on the given element. /// /// The element that has been updated and should be transmitted to the subscribers. /// The user name used to filter the subscribers that will receive the broadcast. /// The type of operation (e.g., create, update, delete) associated with the broadcast. 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); } } /// /// Broadcasts an update notification to all subscribers associated with the specified user. /// Subscribers with admin subscription type receive the user's panel permissions, while other subscribers receive the updated element; if retrieving the admin permissions fails, a null payload is sent as a fallback. /// /// The updated element to send as the notification payload to non-admin subscribers. /// The username whose subscribers will receive the broadcast. /// The operation type that describes the nature of the update. 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); } } } /// /// Updates an existing user together with the associated authorities (roles/permissions), validating that the email and user name remain unique. Deletes, updates, and creates authorities according to the provided DTO, and broadcasts the new permission set whenever at least one authority change has been applied. /// /// DTO containing the user to update along with the lists of authorities to delete, update, and add. /// Flag indicating whether the user's password should be updated as part of the operation. /// The updated . /// Thrown when the underlying user update returns no result. 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; } /// /// Updates the password of an existing user after validating the current password and the strength of the new one, logging the change and recording an audit entry on success. /// /// The unique identifier of the user whose password will be updated. /// The user's current password, used to verify the request before applying the change. /// The new password to set; must differ from the current password and meet the strength policy. /// A task that resolves to true when the password is successfully updated, or false if the repository update returns no result. /// Thrown when is equal to . /// Thrown when no user exists for the supplied . /// Thrown when does not match the user's current password, or when does not satisfy the strength policy. 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; } /// /// Deletes a user by its identifier, records an audit log entry for the deletion, and returns the operation result. /// Returns false if an error occurs during deletion, in which case the exception is logged. /// /// The identifier of the user to delete. /// A task that resolves to true when the user is successfully deleted; otherwise, false if an error is encountered. 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; } } /// /// Retrieves a paginated list of users based on the provided filter, including the total document count for pagination metadata. /// /// The pagination filter containing the page number and page size used to skip, limit, and shape the result set. /// A task that represents the asynchronous operation. The task result contains a with the requested page of users and pagination details. 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); } /// /// Generates a JSON Web Token (JWT) pair (access and refresh) for the specified user, including claims derived from the user's authorization data when available. /// /// The user whose identity, email, IP address, and authorization claims will be embedded in the generated tokens. /// A containing the serialized access and refresh tokens, their respective expiration times, and the associated user. 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 }; } /// /// Retrieves the list of all users by attempting each valid login method in order, selecting the first service that supports password authentication. /// If a service throws a it is remembered and the next service is tried; other exceptions are logged and wrapped in a . /// Throws the captured exception from the last attempt, or when no suitable login service is found. /// /// A task that resolves to the list of objects returned by the first successful login service. /// Thrown when a non-business exception occurs while retrieving users from a service. /// Thrown when no login service matches the valid login methods or all attempts fail without a captured business exception. 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(); } /// /// Authenticates a user using a provided access token and returns a new token pair. /// Validates the token, retrieves the associated user, and either generates a fresh JWT if the original is expired, or issues new access and refresh tokens preserving the remaining lifetime of the supplied token. /// /// The access token to validate and use for user authentication. /// A containing the new access token, refresh token, their expiry times, and the authenticated user. /// Thrown when no user can be resolved from the provided token. 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 }; } /// /// Authenticates a user using a previously issued access token and issues a new JWT. /// Validates the provided token, retrieves the associated user, and loads user authorities on demand before generating the authentication response. /// /// The access token used to identify and authenticate the user. /// A containing the generated JWT for the authenticated user. /// Thrown when no user is found that matches the validated access token. 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); } /// /// Creates a new user authority based on the provided authorization. If the underlying authority service returns a null result, indicating that the creation failed, a conflict exception is raised. /// /// The authorization used to create the new user authority. /// The newly created instance. /// Thrown when the authority service fails to create the user authority (returns null). public async Task CreateNewAuthority(Authorization auth) { var newUserAuthority = await _authorityService.CreateNewUserAuthority(auth) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed); return newUserAuthority; } /// /// Deletes a user authority identified by the specified string identifier. Validates the identifier format and throws if the deletion cannot be completed. /// /// The string representation of the authority's ObjectId to delete. /// A task that resolves to true when the authority is successfully deleted. /// Thrown when the provided is not a valid ObjectId format. /// Thrown when the underlying delete operation fails. 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; } /// /// Updates the authority for the specified authorization entry. If the underlying authority service returns a null result, a conflict is raised to signal that the update could not be applied. /// /// The authorization details used to update the user's authority. /// A task that resolves to true when the authority is successfully updated. /// Thrown when the authority service returns a null response, indicating the update failed. public async Task UpdateAuthority(Authorization authorization) { _ = await _authorityService.EditUserAuthority(authorization) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); return true; } /// /// Asynchronously resolves a list of objects from the provided entries. /// When an authorization has a UnitId, the associated display is looked up and its identifier is assigned to the authorization before the claim is added; otherwise, the claim is added directly. /// /// The list of authorizations to convert into claims. /// A task that represents the asynchronous operation, containing the list of claims built from the supplied authorities. 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; } /// /// Adds a role-based claim to the provided claims list, composing its value from the authorization role and, when present, the display identifier. Skips addition when a claim with the same value already exists to avoid duplicates. /// /// The authorization source whose role and display identifier are used to build the claim value. /// The list of claims to which the new role claim is appended when not already present. 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); } /// /// Verifies whether the specified is already in use, optionally excluding a given user from the check. Throws an exception when a duplicate is found. /// /// The username to validate against existing users. /// Optional identifier of the user being updated; when provided, that user is excluded from the duplicate check. /// Thrown when another user with the same already exists. 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); } /// /// Validates that the specified email is not already in use by another user. When a user ID is provided, the check excludes that user so their existing email remains valid during updates. /// /// The email address to verify for uniqueness in the user collection. /// The optional ID of the current user; when set, it is excluded from the duplicate email check. /// Thrown when the email is already registered to a different user. 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); } }