using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using Microsoft.IdentityModel.Tokens; using NLog; using app.Configuration; using app.Interface.Repositories; using app.Interface.Services; using app.Model; using app.Model.DTOs; using app.Model.Filters; using app.Share.Common; using app.SMTP.Interface; using app.SMTP.Model; namespace app.Service { public class AuthServiceImpl : IAuthService { private readonly IAuthUserRepository _authUserRepository; private readonly IUnitOfWork _unitOfWork; private readonly AuthentificationSettings _authSettings; private readonly ISMTPClient _smtpClient; private readonly FrontendSettings _frontendSettings; private static Logger _logger = LogManager.GetCurrentClassLogger(); private static Random random = new Random(); public AuthServiceImpl( IAuthUserRepository authUserRepository, IUnitOfWork unitOfWork, AuthentificationSettings authSettings, ISMTPClient smtpClient, FrontendSettings frontendSettings ) { _unitOfWork = unitOfWork; _authUserRepository = authUserRepository; _authSettings = authSettings; _smtpClient = smtpClient; _frontendSettings = frontendSettings; } private static string _randomString(int length) { const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; return new string(Enumerable.Repeat(chars, length) .Select(s => s[random.Next(s.Length)]).ToArray()); } private string _generateToken(IEnumerable claims) { var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_authSettings.SecretKey)); var signingCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); var tokenOptions = new JwtSecurityToken( issuer: _authSettings.Issuer, audience: _authSettings.Audience, claims: claims, expires: DateTime.Now.AddMinutes(_authSettings.TokenExpireTime), signingCredentials: signingCredentials ); return new JwtSecurityTokenHandler().WriteToken(tokenOptions); } private string _generateRefreshToken() { var randomNumber = new byte[32]; using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(randomNumber); return Convert.ToBase64String(randomNumber); } } private async Task _createAndSaveRefreshToken(AuthUser user) { var refreshToken = this._generateRefreshToken(); user.RefreshToken = refreshToken; user.RefreshTokenExpiryTime = DateTime.Now.AddDays(_authSettings.RefreshTokenExpireTime); await _unitOfWork.SaveChangesAsync(); return refreshToken; } private ClaimsPrincipal _getPrincipalFromExpiredToken(string token) { var tokenValidationParameters = new TokenValidationParameters { ValidateAudience = false, //you might want to validate the audience and issuer depending on your use case ValidateIssuer = false, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this._authSettings.SecretKey)), ValidateLifetime = false //here we are saying that we don't care about the token's expiration date }; var tokenHandler = new JwtSecurityTokenHandler(); SecurityToken securityToken; var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken); var jwtSecurityToken = securityToken as JwtSecurityToken; if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) throw new SecurityTokenException("Invalid token"); return principal; } private async Task _createAndSaveConfirmationId(AuthUser user) { bool end = false; while (!end) { string id = _randomString(16); var userFromDb = await _authUserRepository.FindAuthUserByEMailConfirmationIdAsync(id); if (userFromDb is null) { end = true; user.ConfirmationId = id; } } } private async Task _createAndSaveForgotPasswordId(AuthUser user) { bool end = false; while (!end) { string id = _randomString(16); var userFromDb = await _authUserRepository.FindAuthUserByEMailForgotPasswordIdAsync(id); if (userFromDb is null) { end = true; user.ForgotPasswordId = id; } } } private async Task _sendConfirmationIdToUser(AuthUser user) { string url = _frontendSettings.URL.EndsWith("/") ? _frontendSettings.URL : $"{_frontendSettings}/"; await _smtpClient.SendEmailAsync(new EMail() { Receiver = user.EMail, Subject = $"E-Mail für {user.FirstName} {user.LastName} bestätigen", Message = $"{url}auth/register/{user.ConfirmationId}" }); } private async Task _sendForgotPasswordIdToUser(AuthUser user) { string url = _frontendSettings.URL.EndsWith("/") ? _frontendSettings.URL : $"{_frontendSettings}/"; await _smtpClient.SendEmailAsync(new EMail() { Receiver = user.EMail, Subject = $"Passwort für {user.FirstName} {user.LastName} zurücksetzen", Message = $"{url}auth/forgot-password/{user.ForgotPasswordId}" }); } public async Task> GetAllAuthUsersAsync() { var authUserDTOs = new List(); var authUsers = await _authUserRepository.GetAllAuthUsersAsync(); authUsers.ForEach(authUser => { authUserDTOs.Add(authUser.ToAuthUserDTO()); }); return authUserDTOs; } public async Task GetFilteredAuthUsersAsync(AuthUserSelectCriterion selectCriterion) { (var users, var totalCount) = await _authUserRepository.GetFilteredAuthUsersAsync(selectCriterion); var result = new List(); users.ForEach(user => { result.Add(user.ToAuthUserDTO()); }); return new GetFilteredAuthUsersResultDTO() { Users = result, TotalCount = totalCount }; } public async Task GetAuthUserByEMailAsync(string email) { try { var authUser = await _authUserRepository.GetAuthUserByEMailAsync(email); return authUser.ToAuthUserDTO(); } catch (Exception e) { _logger.Error(e); throw new ServiceException(ServiceErrorCode.InvalidData, $"AuthUser with email {email} not found"); } } public async Task FindAuthUserByEMailAsync(string email) { var authUser = await _authUserRepository.FindAuthUserByEMailAsync(email); return authUser != null ? authUser.ToAuthUserDTO() : null; } public async Task AddAuthUserAsync(AuthUserDTO authUserDTO) { var authUserDb = await _authUserRepository.FindAuthUserByEMailAsync(authUserDTO.EMail); if (authUserDb != null) { throw new ServiceException(ServiceErrorCode.InvalidUser, "User already exists"); } authUserDTO.Password = ComputeHash(authUserDTO.Password, new SHA256CryptoServiceProvider()); var authUser = authUserDTO.ToAuthUser(); if (!IsValidEmail(authUser.EMail)) { throw new ServiceException(ServiceErrorCode.InvalidData, $"Invalid E-Mail"); } try { _authUserRepository.AddAuthUser(authUser); await _createAndSaveConfirmationId(authUser); await _sendConfirmationIdToUser(authUser); await _unitOfWork.SaveChangesAsync(); _logger.Info($"Added authUser with email: {authUser.EMail}"); return authUser.Id; } catch (Exception e) { _logger.Error(e); throw new ServiceException(ServiceErrorCode.UnableToAdd, $"Cannot add authUser {authUserDTO.EMail}"); } } public async Task ConfirmEMail(string id) { var user = await _authUserRepository.FindAuthUserByEMailConfirmationIdAsync(id); if (user.ConfirmationId == id) { user.ConfirmationId = null; await _unitOfWork.SaveChangesAsync(); return true; } return false; } public async Task Login(AuthUserDTO userDTO) { if (userDTO == null) { throw new ServiceException(ServiceErrorCode.InvalidData, $"User is empty"); } var userFromDb = await _authUserRepository.FindAuthUserByEMailAsync(userDTO.EMail); if (userFromDb == null) { throw new ServiceException(ServiceErrorCode.InvalidUser, "User not found"); } if (userFromDb.ConfirmationId != null) { throw new ServiceException(ServiceErrorCode.InvalidUser, "E-Mail not confirmed"); } userDTO.Password = ComputeHash(userDTO.Password, new SHA256CryptoServiceProvider()); if (userFromDb.Password != userDTO.Password) { throw new ServiceException(ServiceErrorCode.InvalidUser, "Wrong password"); } var tokenString = this._generateToken(new List() { new Claim(ClaimTypes.Name, userDTO.EMail), new Claim(ClaimTypes.Role, userFromDb.AuthRole.ToString()) }); var refreshString = await this._createAndSaveRefreshToken(userFromDb); if (userFromDb.ForgotPasswordId != null) { userFromDb.ForgotPasswordId = null; await _unitOfWork.SaveChangesAsync(); } return new TokenDTO { Token = tokenString, RefreshToken = refreshString }; } public async Task ForgotPassword(string email) { var user = await _authUserRepository.FindAuthUserByEMailAsync(email); if (user is null) { return; } await _createAndSaveForgotPasswordId(user); await _sendForgotPasswordIdToUser(user); await _unitOfWork.SaveChangesAsync(); } public async Task ConfirmForgotPassword(string id) { var user = await _authUserRepository.FindAuthUserByEMailForgotPasswordIdAsync(id); return new EMailStringDTO() { EMail = user.EMail }; } public async Task ResetPassword(ResetPasswordDTO rpDTO) { var user = await _authUserRepository.FindAuthUserByEMailForgotPasswordIdAsync(rpDTO.Id); if (user == null) { throw new ServiceException(ServiceErrorCode.InvalidUser, "User not found"); } if (user.ConfirmationId != null) { throw new ServiceException(ServiceErrorCode.InvalidUser, "E-Mail not confirmed"); } if (rpDTO.Password == null || rpDTO.Password == "") { throw new ServiceException(ServiceErrorCode.InvalidData, "Password is empty"); } rpDTO.Password = ComputeHash(rpDTO.Password, new SHA256CryptoServiceProvider()); user.Password = rpDTO.Password; await _unitOfWork.SaveChangesAsync(); } public async Task UpdateUser(UpdateUserDTO updateUserDTO) { if (updateUserDTO == null) { throw new ServiceException(ServiceErrorCode.InvalidData, $"User is empty"); } if (updateUserDTO.AuthUserDTO == null) { throw new ServiceException(ServiceErrorCode.InvalidData, $"Existing user is empty"); } if (updateUserDTO.NewAuthUserDTO == null) { throw new ServiceException(ServiceErrorCode.InvalidData, $"New user is empty"); } if (!IsValidEmail(updateUserDTO.AuthUserDTO.EMail) || !IsValidEmail(updateUserDTO.NewAuthUserDTO.EMail)) { throw new ServiceException(ServiceErrorCode.InvalidData, $"Invalid E-Mail"); } var user = await _authUserRepository.FindAuthUserByEMailAsync(updateUserDTO.AuthUserDTO.EMail); if (user == null) { throw new ServiceException(ServiceErrorCode.InvalidUser, "User not found"); } if (user.ConfirmationId != null) { throw new ServiceException(ServiceErrorCode.InvalidUser, "E-Mail not confirmed"); } // update first name if (updateUserDTO.NewAuthUserDTO.FirstName != null && updateUserDTO.AuthUserDTO.FirstName != updateUserDTO.NewAuthUserDTO.FirstName) { user.FirstName = updateUserDTO.NewAuthUserDTO.FirstName; } // update last name if (updateUserDTO.NewAuthUserDTO.LastName != null && updateUserDTO.NewAuthUserDTO.LastName != "" && updateUserDTO.AuthUserDTO.LastName != updateUserDTO.NewAuthUserDTO.LastName) { user.LastName = updateUserDTO.NewAuthUserDTO.LastName; } // update E-Mail if (updateUserDTO.NewAuthUserDTO.EMail != null && updateUserDTO.NewAuthUserDTO.EMail != "" && updateUserDTO.AuthUserDTO.EMail != updateUserDTO.NewAuthUserDTO.EMail) { var userByNewEMail = await _authUserRepository.FindAuthUserByEMailAsync(updateUserDTO.NewAuthUserDTO.EMail); if (userByNewEMail != null) { throw new ServiceException(ServiceErrorCode.InvalidUser, "User already exists"); } user.EMail = updateUserDTO.NewAuthUserDTO.EMail; } bool isExistingPasswordSet = false; bool isnewPasswordSet = false; // hash passwords in DTOs if (updateUserDTO.AuthUserDTO.Password != null && updateUserDTO.AuthUserDTO.Password != "") { isExistingPasswordSet = true; updateUserDTO.AuthUserDTO.Password = ComputeHash(updateUserDTO.AuthUserDTO.Password, new SHA256CryptoServiceProvider()); } if (updateUserDTO.AuthUserDTO.Password != user.Password) { throw new ServiceException(ServiceErrorCode.InvalidUser, "Wrong password"); } if (updateUserDTO.NewAuthUserDTO.Password != null && updateUserDTO.NewAuthUserDTO.Password != "") { isnewPasswordSet = true; updateUserDTO.NewAuthUserDTO.Password = ComputeHash(updateUserDTO.NewAuthUserDTO.Password, new SHA256CryptoServiceProvider()); } // update password if (isExistingPasswordSet && isnewPasswordSet && updateUserDTO.AuthUserDTO.Password != updateUserDTO.NewAuthUserDTO.Password) { user.Password = updateUserDTO.NewAuthUserDTO.Password; } await _unitOfWork.SaveChangesAsync(); } public async Task UpdateUserAsAdmin(AdminUpdateUserDTO updateUserDTO) { if (updateUserDTO == null) { throw new ServiceException(ServiceErrorCode.InvalidData, $"User is empty"); } if (updateUserDTO.AuthUserDTO == null) { throw new ServiceException(ServiceErrorCode.InvalidData, $"Existing user is empty"); } if (updateUserDTO.NewAuthUserDTO == null) { throw new ServiceException(ServiceErrorCode.InvalidData, $"New user is empty"); } if (!IsValidEmail(updateUserDTO.AuthUserDTO.EMail) || !IsValidEmail(updateUserDTO.NewAuthUserDTO.EMail)) { throw new ServiceException(ServiceErrorCode.InvalidData, $"Invalid E-Mail"); } var user = await _authUserRepository.FindAuthUserByEMailAsync(updateUserDTO.AuthUserDTO.EMail); if (user == null) { throw new ServiceException(ServiceErrorCode.InvalidUser, "User not found"); } if (user.ConfirmationId != null && updateUserDTO.NewAuthUserDTO.IsConfirmed) { user.ConfirmationId = null; } else if (user.ConfirmationId == null && !updateUserDTO.NewAuthUserDTO.IsConfirmed) { await _createAndSaveConfirmationId(user); } // else // { // throw new ServiceException(ServiceErrorCode.InvalidUser, "E-Mail not confirmed"); // } // update first name if (updateUserDTO.NewAuthUserDTO.FirstName != null && updateUserDTO.AuthUserDTO.FirstName != updateUserDTO.NewAuthUserDTO.FirstName) { user.FirstName = updateUserDTO.NewAuthUserDTO.FirstName; } // update last name if (updateUserDTO.NewAuthUserDTO.LastName != null && updateUserDTO.NewAuthUserDTO.LastName != "" && updateUserDTO.AuthUserDTO.LastName != updateUserDTO.NewAuthUserDTO.LastName) { user.LastName = updateUserDTO.NewAuthUserDTO.LastName; } // update E-Mail if (updateUserDTO.NewAuthUserDTO.EMail != null && updateUserDTO.NewAuthUserDTO.EMail != "" && updateUserDTO.AuthUserDTO.EMail != updateUserDTO.NewAuthUserDTO.EMail) { var userByNewEMail = await _authUserRepository.FindAuthUserByEMailAsync(updateUserDTO.NewAuthUserDTO.EMail); if (userByNewEMail != null) { throw new ServiceException(ServiceErrorCode.InvalidUser, "User already exists"); } user.EMail = updateUserDTO.NewAuthUserDTO.EMail; } // update password if (updateUserDTO.ChangePassword && updateUserDTO.AuthUserDTO.Password != updateUserDTO.NewAuthUserDTO.Password) { user.Password = ComputeHash(updateUserDTO.NewAuthUserDTO.Password, new SHA256CryptoServiceProvider()); } // update role if (user.AuthRole == updateUserDTO.AuthUserDTO.AuthRole && user.AuthRole != updateUserDTO.NewAuthUserDTO.AuthRole) { user.AuthRole = updateUserDTO.NewAuthUserDTO.AuthRole; } await _unitOfWork.SaveChangesAsync(); } public async Task Refresh(TokenDTO tokenDTO) { if (tokenDTO is null) { throw new ServiceException(ServiceErrorCode.InvalidData, $"Token is empty"); } var principal = this._getPrincipalFromExpiredToken(tokenDTO.Token); var email = principal.Identity.Name; var user = await this._authUserRepository.FindAuthUserByEMailAsync(email); if (user == null || user.RefreshToken != tokenDTO.RefreshToken || user.RefreshTokenExpiryTime <= DateTime.Now) { throw new ServiceException(ServiceErrorCode.InvalidData, $"Token is expired"); } var newToken = this._generateToken(principal.Claims); var newRefreshToken = await this._createAndSaveRefreshToken(user); return new TokenDTO() { Token = newToken, RefreshToken = newRefreshToken }; } public async Task Revoke(TokenDTO tokenDTO) { if (tokenDTO == null || tokenDTO.Token == null || tokenDTO.RefreshToken == null) { throw new ServiceException(ServiceErrorCode.InvalidData, $"Token is empty"); }; var principal = this._getPrincipalFromExpiredToken(tokenDTO.Token); var email = principal.Identity.Name; var user = await this._authUserRepository.FindAuthUserByEMailAsync(email); if (user == null || user.RefreshToken != tokenDTO.RefreshToken || user.RefreshTokenExpiryTime <= DateTime.Now) { throw new ServiceException(ServiceErrorCode.InvalidData, $"Token is expired"); } user.RefreshToken = null; await _unitOfWork.SaveChangesAsync(); } public async Task DeleteAuthUserByEMailAsync(string email) { try { await _authUserRepository.DeleteAuthUserByEMailAsync(email); await _unitOfWork.SaveChangesAsync(); _logger.Info($"Deleted authUser with email: {email}"); } catch (Exception e) { _logger.Error(e); throw new ServiceException(ServiceErrorCode.UnableToDelete, $"Cannot delete authUser with email {email}"); } } public async Task DeleteAuthUserAsync(AuthUserDTO authUserDTO) { try { _authUserRepository.DeleteAuthUser(authUserDTO.ToAuthUser()); await _unitOfWork.SaveChangesAsync(); _logger.Info($"Deleted authUser {authUserDTO.EMail}"); } catch (Exception e) { _logger.Error(e); throw new ServiceException(ServiceErrorCode.UnableToDelete, $"Cannot delete authUser {authUserDTO.EMail}"); } } private string ComputeHash(string input, HashAlgorithm algorithm) { Byte[] inputBytes = Encoding.UTF8.GetBytes(input); Byte[] hashedBytes = algorithm.ComputeHash(inputBytes); return BitConverter.ToString(hashedBytes); } private bool IsValidEmail(string email) { try { var addr = new System.Net.Mail.MailAddress(email); return addr.Address == email; } catch { return false; } } } }