This repository has been archived on 2023-02-13. You can view files and clone it, but cannot push or open issues or pull requests.
gswi-server/gswi.Service/AuthServiceImpl.cs

504 lines
22 KiB
C#

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 gswi.Configuration;
using gswi.Interface.Repositories;
using gswi.Interface.Services;
using gswi.Model;
using gswi.Model.DTOs;
using gswi.Model.Filters;
using gswi.Share.Common;
using gswi.SMTP.Interface;
using gswi.SMTP.Model;
using Microsoft.IdentityModel.Tokens;
using NLog;
namespace gswi.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<Claim> 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<string> _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<List<AuthUserDTO>> GetAllAuthUsersAsync() {
var authUserDTOs = new List<AuthUserDTO>();
var authUsers = await _authUserRepository.GetAllAuthUsersAsync();
authUsers.ForEach(authUser => {
authUserDTOs.Add(authUser.ToAuthUserDTO());
});
return authUserDTOs;
}
public async Task<GetFilteredAuthUsersResultDTO> GetFilteredAuthUsersAsync(AuthUserSelectCriterion selectCriterion) {
(var users, var totalCount) = await _authUserRepository.GetFilteredAuthUsersAsync(selectCriterion);
var result = new List<AuthUserDTO>();
users.ForEach(user => {
result.Add(user.ToAuthUserDTO());
});
return new GetFilteredAuthUsersResultDTO() {
Users = result,
TotalCount = totalCount
};
}
public async Task<AuthUserDTO> 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<AuthUserDTO> FindAuthUserByEMailAsync(string email) {
var authUser = await _authUserRepository.FindAuthUserByEMailAsync(email);
return authUser != null ? authUser.ToAuthUserDTO() : null;
}
public async Task<long> 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<bool> 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<TokenDTO> 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<Claim>() {
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<EMailStringDTO> 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<TokenDTO> 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;
}
}
}
}