using Microsoft.AspNetCore.WebUtilities; namespace Diligent.WebAPI.Business.Services { public class AuthenticationService : IAuthenticationService { private readonly AuthorizationSettings _authSettings; private readonly FrontEndSettings _frontEndSettings; private readonly UserManager _userManager; private readonly DatabaseContext _databaseContext; private readonly IEmailer _emailer; private readonly ILogger _logger; private readonly IHttpClientService _httpClient; public AuthenticationService(IOptions authSettings, IOptions frontEndSettings, UserManager userManager, DatabaseContext databaseContext, IEmailer emailer, ILogger logger, IHttpClientService httpClient) { _authSettings = authSettings.Value; _frontEndSettings = frontEndSettings.Value; _userManager = userManager; _databaseContext = databaseContext; _httpClient = httpClient; _emailer = emailer; _logger = logger; } public async Task> Authenticate(AuthenticateRequestDto model) { _logger.LogError($"Checking credentials for user: {model.Username}"); var user = await _userManager.FindByNameAsync(model.Username); // return null if user not found if (user == null) { _logger.LogError($"User with username = {model.Username} not found"); return new ServiceResponseDTO { IsError = true, ErrorMessage = "Username is not valid" }; } var result = await _userManager.CheckPasswordAsync(user, model.Password); // return null if user is disabled if (user.IsEnabled == false) { _logger.LogError($"User: {model.Username} is not enabled"); return new ServiceResponseDTO { IsError = true, ErrorMessage = $"User with email {model.Username} has no permission to log in." }; } // password is not correct if (!result) { _logger.LogError($"Password for user: {model.Username} is not correct"); await _userManager.AccessFailedAsync(user); return new ServiceResponseDTO { IsError = true, ErrorMessage = "Password is not correct" }; } var token = await GenerateToken(user); _logger.LogError($"Successfull login token: {token}"); return token; } public async Task> Authenticate(GoogleApiModel model) { _logger.LogError($"Checking token for google login {model.Token}"); if (!(await _httpClient.IsTokenValid(model.Token))) { _logger.LogError($"Token is not valid"); return new ServiceResponseDTO { IsError = true, ErrorMessage = "Invalid Google Api Token" }; } _logger.LogError($"Checking if user exists in Db with email : {model.User.email}"); var user = await _userManager.FindByEmailAsync(model.User.email); // return null if user not found if (user == null) { _logger.LogError($"User does not exist in Db"); return new ServiceResponseDTO { IsError = true, ErrorMessage = $"User with email {model.User.email} does not exist in database" }; } if (user.IsEnabled == false) { _logger.LogError($"User is not enabled"); return new ServiceResponseDTO { IsError = true, ErrorMessage = $"User with email {model.User.email} has no permission to log in." }; } var token = await GenerateToken(user); _logger.LogInformation($"Successfull login. Token :{token}"); return token; } private async Task> GenerateToken(User user) { var isLocked = await _userManager.IsLockedOutAsync(user); if (isLocked) return new ServiceResponseDTO { IsError = true, ErrorMessage = "The account is locked out" }; // authentication successful so generate jwt token var token = await GenerateJwtToken(user, true); var data = new AuthenticateResponseDto { Id = user.Id, Username = user.UserName, FirstName = user.FirstName, LastName = user.LastName, Token = token, RefreshToken = token }; return new ServiceResponseDTO { Data = data }; } private async Task GenerateJwtToken(User user, bool authenticate = false) { // generate token that is valid for 7 days var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_authSettings.Secret); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { new Claim(JwtRegisteredClaimNames.Jti, user.Id.ToString()), new Claim("id", user.Id.ToString()) }), Expires = DateTime.UtcNow.AddMinutes(_authSettings.JwtExpiredTime), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); var writedToken = tokenHandler.WriteToken(token); var refreshToken = new RefreshToken { Token = writedToken, JwtId = user.Id.ToString(), UserId = user.Id, User = user, CreationDate = DateTime.UtcNow, ExpiryDate = DateTime.UtcNow.AddMinutes(_authSettings.JwtRefreshExpiredTime) }; var existRefreshToken = await _databaseContext.RefreshTokens.Where(x => x.UserId == user.Id).FirstOrDefaultAsync(); if (existRefreshToken != null) { existRefreshToken.Token = writedToken; existRefreshToken.JwtId = token.Id; existRefreshToken.CreationDate = DateTime.UtcNow; existRefreshToken.ExpiryDate = DateTime.UtcNow.AddMinutes(_authSettings.JwtRefreshExpiredTime); if (authenticate) { existRefreshToken.Used = false; existRefreshToken.Invalidated = false; } _databaseContext.RefreshTokens.Update(existRefreshToken); await UpdateRefreshToken(existRefreshToken); } else { await _databaseContext.RefreshTokens.AddAsync(refreshToken); } await _databaseContext.SaveChangesAsync(); _logger.LogInformation($"JWTToken : {writedToken}"); return writedToken; } public async Task RefreshTokenAsync(RefreshTokenRequestDto model) { var validatedToken = GetPrincipalFromToken(model.Token, false); if (validatedToken == null) { return new RefreshTokenResultDto { Error = "Invalid token" }; } var jti = validatedToken.Claims.Single(x => x.Type == JwtRegisteredClaimNames.Jti).Value; var storedRefreshToken = await _databaseContext.RefreshTokens.SingleOrDefaultAsync(x => x.JwtId == jti); if (storedRefreshToken == null) { return new RefreshTokenResultDto { Error = "This refresh token does not exist" }; } var userk = await _databaseContext.Users.Where(u => u.Id == storedRefreshToken.UserId).FirstOrDefaultAsync(); if (userk == null) { return new RefreshTokenResultDto { Error = "There is no user which is associated with refresh token" }; } var expiryDateUnix = long.Parse(validatedToken.Claims.Single(x => x.Type == JwtRegisteredClaimNames.Exp).Value); var expiryDateTimeUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) .AddMinutes(expiryDateUnix); if (expiryDateTimeUtc < DateTime.UtcNow) { return new RefreshTokenResultDto { Data = new AuthenticateResponseDto { Id = userk.Id, FirstName = userk.FirstName, LastName = userk.LastName, Username = userk.UserName, Token = model.Token, RefreshToken = model.RefreshToken } }; } if (DateTime.UtcNow > storedRefreshToken.ExpiryDate) { return new RefreshTokenResultDto { Error = "This refresh token has expired" }; } if (storedRefreshToken.Invalidated) { return new RefreshTokenResultDto { Error = "This refresh token has been invalidated" }; } if (storedRefreshToken.JwtId != jti) { return new RefreshTokenResultDto { Error = "This refresh token does not match this JWT" }; } storedRefreshToken.ExpiryDate = DateTime.UtcNow.AddMinutes(_authSettings.JwtRefreshExpiredTime); await _databaseContext.SaveChangesAsync(); var user = await _userManager.FindByIdAsync(validatedToken.Claims.Single(x => x.Type == "id").Value); var token = await GenerateJwtToken(user); _logger.LogInformation($"Refresh token : {model.Token}"); return new RefreshTokenResultDto { Data = new AuthenticateResponseDto { Id = userk.Id, FirstName = userk.FirstName, LastName = userk.LastName, Username = userk.UserName, Token = token, RefreshToken = token } }; } public async Task> DeleteRefreshToken(int userId) { var refreshToken = await _databaseContext.RefreshTokens.Where(r => r.UserId == userId).FirstOrDefaultAsync(); if (refreshToken is null) return new ServiceResponseDTO { IsError = true, ErrorMessage = "There is no refresh token for user" }; _databaseContext.RefreshTokens.Remove(refreshToken); var result = await _databaseContext.SaveChangesAsync() > 0; if (!result) return new ServiceResponseDTO { IsError = true, ErrorMessage = "Problem with saving changes into database" }; _logger.LogInformation($"Delted refresh token : {refreshToken}"); return new ServiceResponseDTO { Data = null }; } private ClaimsPrincipal? GetPrincipalFromToken(string token, bool validateLifetime) { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_authSettings.Secret); var tokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = false, ValidateAudience = false, RequireExpirationTime = false, ValidateLifetime = validateLifetime, // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later) //ClockSkew = TimeSpan.Zero }; try { var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var validatedToken); if (!IsJwtWithValidSecurityAlgorithm(validatedToken)) { return null; } return principal; } catch (Exception) { return null; } } private static bool IsJwtWithValidSecurityAlgorithm(SecurityToken validatedToken) { return (validatedToken is JwtSecurityToken jwtSecurityToken) && jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase); } public async Task GetRefreshTokenByUserId(int userId) { return await _databaseContext.RefreshTokens.Where(x => x.UserId == userId).FirstOrDefaultAsync(); } public async Task UpdateRefreshToken(RefreshToken refreshToken) { _databaseContext.RefreshTokens.Update(refreshToken); await _databaseContext.SaveChangesAsync(); } public async Task> GetForgotPasswordUrlAsync(string email) { _logger.LogInformation($"Sending forgot password email for : {email}"); var user = await _userManager.FindByEmailAsync(email); if (user == null) { return new ServiceResponseDTO { IsError = true, ErrorMessage = "Email did not find." }; } var token = await _userManager.GeneratePasswordResetTokenAsync(user); token = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(token)); await _emailer.SendEmailAndWriteToDbAsync(email, "Reset password", HTMLHelper.RenderForgotPasswordPage($"{_frontEndSettings.BaseUrl}/reset-password?token={token}&email={email}"), isHtml: true); _logger.LogInformation($"Reset password email is sent"); return new ServiceResponseDTO { Data = new { code = token, email = email } }; } public async Task> PasswordResetAsync(string email, string code, string password) { _logger.LogInformation($"User with email : {email} changes password"); var user = await _userManager.FindByEmailAsync(email); if (user == null) { return new ServiceResponseDTO { IsError = true, ErrorMessage = "Email did not find." }; } var passwordResetToken = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)); IdentityResult resetResult = await _userManager.ResetPasswordAsync(user, passwordResetToken, password); if (resetResult.Succeeded) { _logger.LogInformation($"Password for user : {email} changed successfully"); return new ServiceResponseDTO { Data = true }; } var errors = resetResult.Errors.Select(x => x.Description); return new ServiceResponseDTO { IsError = true, ErrorMessage = errors.First() }; } } }