| @@ -0,0 +1,30 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Diligent.WebAPI.Business.Helper | |||
| { | |||
| public static class HTMLHelper | |||
| { | |||
| public static string RenderForgotPasswordPage(string url) | |||
| { | |||
| return "<div style=\"font-family: sans-serif\">" + | |||
| "<div style=\"font-family: sans-serif;text-align: center;\">" + | |||
| "<h2 style=\"color: #017397;\">HR Center Password Reset</h2>" + | |||
| "<p style=\"font-size: 20px\">" + | |||
| "To reset your HR Center password, please click on the button below." + | |||
| "</p>" + | |||
| "<a style = \"color: white;text-decoration:none;background-color: #017397;cursor: pointer;font-size: 20px;width: 220px;text-align: center;border-radius: 5px;padding: 5px 15px;height: 25px;\" " + | |||
| $"href=\"{url}\">" + | |||
| " RESET PASSWORD" + | |||
| "</a>" + | |||
| "<p style = \"font-size: 12px; margin-top: 25px;\" >" + | |||
| "Please do not reply to this email.This message was sent from a notification-only address that is not monitored." + | |||
| "</p>" + | |||
| "</div>" + | |||
| "</div>"; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,5 +1,4 @@ | |||
| | |||
| namespace Diligent.WebAPI.Business.Services | |||
| namespace Diligent.WebAPI.Business.Services | |||
| { | |||
| public class AdService : IAdService | |||
| { | |||
| @@ -0,0 +1,400 @@ | |||
| using Microsoft.AspNetCore.WebUtilities; | |||
| using Microsoft.Extensions.Logging; | |||
| using System.Net; | |||
| namespace Diligent.WebAPI.Business.Services | |||
| { | |||
| public class AuthenticationService : IAuthenticationService | |||
| { | |||
| private readonly AuthorizationSettings _authSettings; | |||
| private readonly FrontEndSettings _frontEndSettings; | |||
| private readonly UserManager<User> _userManager; | |||
| private readonly DatabaseContext _databaseContext; | |||
| private readonly IEmailer _emailer; | |||
| private readonly ILogger<AuthenticationService> _logger; | |||
| private readonly IHttpClientService _httpClient; | |||
| public AuthenticationService(IOptions<AuthorizationSettings> authSettings, | |||
| IOptions<FrontEndSettings> frontEndSettings, | |||
| UserManager<User> userManager, | |||
| DatabaseContext databaseContext, | |||
| IEmailer emailer, | |||
| ILogger<AuthenticationService> logger, | |||
| IHttpClientService httpClient) | |||
| { | |||
| _authSettings = authSettings.Value; | |||
| _frontEndSettings = frontEndSettings.Value; | |||
| _userManager = userManager; | |||
| _databaseContext = databaseContext; | |||
| _httpClient = httpClient; | |||
| _emailer = emailer; | |||
| _logger = logger; | |||
| } | |||
| public async Task<ServiceResponseDTO<AuthenticateResponseDto>> Authenticate(AuthenticateRequestDto model) | |||
| { | |||
| var user = await _userManager.FindByNameAsync(model.Username); | |||
| // return null if user not found | |||
| if (user == null) | |||
| { | |||
| return new ServiceResponseDTO<AuthenticateResponseDto> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Username is not valid" | |||
| }; | |||
| } | |||
| var result = await _userManager.CheckPasswordAsync(user, model.Password); | |||
| // password is not correct | |||
| if (!result) | |||
| { | |||
| await _userManager.AccessFailedAsync(user); | |||
| return new ServiceResponseDTO<AuthenticateResponseDto> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Password is not correct" | |||
| }; | |||
| } | |||
| return await GenerateToken(user); | |||
| } | |||
| public async Task<ServiceResponseDTO<AuthenticateResponseDto>> Authenticate(GoogleApiModel model) | |||
| { | |||
| if (!(await _httpClient.IsTokenValid(model.Token))) | |||
| { | |||
| return new ServiceResponseDTO<AuthenticateResponseDto> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Invalid Google Api Token" | |||
| }; | |||
| } | |||
| var user = await _userManager.FindByEmailAsync(model.User.email); | |||
| // return null if user not found | |||
| if (user == null) | |||
| { | |||
| return new ServiceResponseDTO<AuthenticateResponseDto> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = $"User with email {model.User.email} does not exist in database" | |||
| }; | |||
| } | |||
| return await GenerateToken(user); | |||
| } | |||
| private async Task<ServiceResponseDTO<AuthenticateResponseDto>> GenerateToken(User user) | |||
| { | |||
| var isLocked = await _userManager.IsLockedOutAsync(user); | |||
| if (isLocked) | |||
| return new ServiceResponseDTO<AuthenticateResponseDto> | |||
| { | |||
| 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<AuthenticateResponseDto> | |||
| { | |||
| Data = data | |||
| }; | |||
| } | |||
| private async Task<string> 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(); | |||
| return writedToken; | |||
| } | |||
| public async Task<RefreshTokenResultDto> 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); | |||
| return new RefreshTokenResultDto | |||
| { | |||
| Data = new AuthenticateResponseDto | |||
| { | |||
| Id = userk.Id, | |||
| FirstName = userk.FirstName, | |||
| LastName = userk.LastName, | |||
| Username = userk.UserName, | |||
| Token = model.Token, | |||
| RefreshToken = model.RefreshToken | |||
| } | |||
| }; | |||
| } | |||
| public async Task<ServiceResponseDTO<string>> DeleteRefreshToken(int userId) | |||
| { | |||
| var refreshToken = await _databaseContext.RefreshTokens.Where(r => r.UserId == userId).FirstOrDefaultAsync(); | |||
| if (refreshToken is null) | |||
| return new ServiceResponseDTO<string> | |||
| { | |||
| 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<string> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Problem with saving changes into database" | |||
| }; | |||
| return new ServiceResponseDTO<string> | |||
| { | |||
| 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<RefreshToken?> 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<ServiceResponseDTO<object>> GetEmailConfirmationUrlAsync(string email) | |||
| { | |||
| var user = await _userManager.FindByEmailAsync(email); | |||
| if (user == null) | |||
| { | |||
| return new ServiceResponseDTO<object> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Email did not find." | |||
| }; | |||
| } | |||
| var token = await _userManager.GenerateEmailConfirmationTokenAsync(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); | |||
| user.PasswordResetToken = token; | |||
| await _databaseContext.SaveChangesAsync(); | |||
| return new ServiceResponseDTO<object> | |||
| { | |||
| Data = new { code = token, email = email } | |||
| }; | |||
| } | |||
| public async Task<ServiceResponseDTO<object>> PasswordResetAsync(string email, string code, string password) | |||
| { | |||
| var user = await _userManager.FindByEmailAsync(email); | |||
| if (user == null) | |||
| { | |||
| return new ServiceResponseDTO<object> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Email did not find." | |||
| }; | |||
| } | |||
| // FOR SOME REASON USERMANAGER.RESETPASSWORDASYNC returns InvalidToken. In future change this | |||
| //var passwordResetToken = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)); | |||
| //IdentityResult resetResult = await _userManager.ResetPasswordAsync(user, passwordResetToken, password); | |||
| //if (resetResult.Succeeded) | |||
| await _userManager.RemovePasswordAsync(user); | |||
| await _userManager.AddPasswordAsync(user, password); | |||
| if (user.PasswordResetToken == code) | |||
| { | |||
| if (await _userManager.IsLockedOutAsync(user)) | |||
| { | |||
| await _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow); | |||
| } | |||
| return new ServiceResponseDTO<object> { Data = true }; | |||
| } | |||
| //var errors = resetResult.Errors.Select(x => x.Description); | |||
| return new ServiceResponseDTO<object> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Invalid reset password token" | |||
| }; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,46 @@ | |||
| using System.Net; | |||
| namespace Diligent.WebAPI.Business.Services | |||
| { | |||
| public class HttpClientService : IHttpClientService | |||
| { | |||
| private const string GoogleApiTokenInfoUrl = "https://www.googleapis.com/oauth2/v3/tokeninfo?id_token={0}"; | |||
| private string[] SupportedClientsIds = { "" }; | |||
| private readonly AuthorizationSettings _authSettings; | |||
| public HttpClientService(IOptions<AuthorizationSettings> authSettings) | |||
| { | |||
| _authSettings = authSettings.Value; | |||
| } | |||
| public async Task<bool> IsTokenValid(string providerToken) | |||
| { | |||
| var httpClient = new HttpClient(); | |||
| var requestUri = new Uri(string.Format(GoogleApiTokenInfoUrl, providerToken)); | |||
| HttpResponseMessage httpResponseMessage; | |||
| try | |||
| { | |||
| httpResponseMessage = httpClient.GetAsync(requestUri).Result; | |||
| } | |||
| catch | |||
| { | |||
| return false; | |||
| } | |||
| if (httpResponseMessage.StatusCode != HttpStatusCode.OK) | |||
| { | |||
| return false; | |||
| } | |||
| var response = httpResponseMessage.Content.ReadAsStringAsync().Result; | |||
| var googleApiTokenInfo = JsonConvert.DeserializeObject<GoogleApiTokenInfo>(response); | |||
| //if (!SupportedClientsIds.Contains(googleApiTokenInfo.aud)) | |||
| if (googleApiTokenInfo.aud != _authSettings.GoogleClientId) | |||
| { | |||
| return false; | |||
| } | |||
| return true; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| namespace Diligent.WebAPI.Business.Services.Interfaces | |||
| { | |||
| public interface IAuthenticationService | |||
| { | |||
| Task<ServiceResponseDTO<AuthenticateResponseDto>> Authenticate(AuthenticateRequestDto model); | |||
| Task<ServiceResponseDTO<AuthenticateResponseDto>> Authenticate(GoogleApiModel model); | |||
| Task<RefreshTokenResultDto> RefreshTokenAsync(RefreshTokenRequestDto model); | |||
| Task<RefreshToken?> GetRefreshTokenByUserId(int userId); | |||
| Task UpdateRefreshToken(RefreshToken refreshToken); | |||
| Task<ServiceResponseDTO<string>> DeleteRefreshToken(int userId); | |||
| Task<ServiceResponseDTO<object>> GetEmailConfirmationUrlAsync(string email); | |||
| Task<ServiceResponseDTO<object>> PasswordResetAsync(string email, string code, string password); | |||
| } | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Diligent.WebAPI.Business.Services.Interfaces | |||
| { | |||
| public interface IHttpClientService | |||
| { | |||
| Task<bool> IsTokenValid(string providedToken); | |||
| } | |||
| } | |||
| @@ -2,25 +2,11 @@ | |||
| { | |||
| public interface IUserService | |||
| { | |||
| Task<ServiceResponseDTO<AuthenticateResponseDto>> Authenticate(AuthenticateRequestDto model); | |||
| Task<ServiceResponseDTO<AuthenticateResponseDto>> Authenticate(GoogleApiModel model); | |||
| Task<RefreshTokenResultDto> RefreshTokenAsync(RefreshTokenRequestDto model); | |||
| Task<IEnumerable<User?>> GetAll(); | |||
| Task<User?> GetById(int id); | |||
| Task CreateUser(CreateUserRequestDto model); | |||
| Task<RefreshToken?> GetRefreshTokenByUserId(int userId); | |||
| Task UpdateRefreshToken(RefreshToken refreshToken); | |||
| Task<ServiceResponseDTO<string>> DeleteRefreshToken(int userId); | |||
| Task<ServiceResponseDTO<object>> GetEmailConfirmationUrlAsync(string email); | |||
| Task<ServiceResponseDTO<object>> PasswordResetAsync(string email, string code, string password); | |||
| } | |||
| } | |||
| @@ -1,6 +1,8 @@ | |||
| using Microsoft.AspNetCore.WebUtilities; | |||
| using Diligent.WebAPI.Business.Services.Interfaces; | |||
| using Diligent.WebAPI.Business.Settings; | |||
| using Diligent.WebAPI.Data; | |||
| using Microsoft.AspNetCore.Identity; | |||
| using Microsoft.Extensions.Logging; | |||
| using System.Net; | |||
| namespace Diligent.WebAPI.Business.Services | |||
| { | |||
| @@ -40,425 +42,5 @@ namespace Diligent.WebAPI.Business.Services | |||
| await _userManager.CreateAsync(user, model.Password); | |||
| } | |||
| private bool IsTokenValid(string providerToken) | |||
| { | |||
| var httpClient = new HttpClient(); | |||
| var requestUri = new Uri(string.Format(GoogleApiTokenInfoUrl, providerToken)); | |||
| HttpResponseMessage httpResponseMessage; | |||
| try | |||
| { | |||
| httpResponseMessage = httpClient.GetAsync(requestUri).Result; | |||
| } | |||
| catch (Exception ex) | |||
| { | |||
| return false; | |||
| } | |||
| if (httpResponseMessage.StatusCode != HttpStatusCode.OK) | |||
| { | |||
| return false; | |||
| } | |||
| var response = httpResponseMessage.Content.ReadAsStringAsync().Result; | |||
| var googleApiTokenInfo = JsonConvert.DeserializeObject<GoogleApiTokenInfo>(response); | |||
| //if (!SupportedClientsIds.Contains(googleApiTokenInfo.aud)) | |||
| if(googleApiTokenInfo.aud != _authSettings.GoogleClientId) | |||
| { | |||
| return false; | |||
| } | |||
| return true; | |||
| } | |||
| public async Task<ServiceResponseDTO<AuthenticateResponseDto>> Authenticate(AuthenticateRequestDto model) | |||
| { | |||
| var user = await _userManager.FindByNameAsync(model.Username); | |||
| // return null if user not found | |||
| if (user == null) | |||
| { | |||
| return new ServiceResponseDTO<AuthenticateResponseDto> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Username is not valid" | |||
| }; | |||
| } | |||
| var result = await _userManager.CheckPasswordAsync(user, model.Password); | |||
| // password is not correct | |||
| if (!result) | |||
| { | |||
| await _userManager.AccessFailedAsync(user); | |||
| return new ServiceResponseDTO<AuthenticateResponseDto> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Password is not correct" | |||
| }; | |||
| } | |||
| return await GenerateToken(user); | |||
| } | |||
| public async Task<ServiceResponseDTO<AuthenticateResponseDto>> Authenticate(GoogleApiModel model) | |||
| { | |||
| if (!IsTokenValid(model.Token)) | |||
| { | |||
| return new ServiceResponseDTO<AuthenticateResponseDto> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Invalid Google Api Token" | |||
| }; | |||
| } | |||
| var user = await _userManager.FindByEmailAsync(model.User.email); | |||
| // return null if user not found | |||
| if (user == null) | |||
| { | |||
| return new ServiceResponseDTO<AuthenticateResponseDto> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = $"User with email {model.User.email} does not exist in database" | |||
| }; | |||
| } | |||
| return await GenerateToken(user); | |||
| } | |||
| private async Task<ServiceResponseDTO<AuthenticateResponseDto>> GenerateToken(User user) | |||
| { | |||
| var isLocked = await _userManager.IsLockedOutAsync(user); | |||
| if (isLocked) | |||
| return new ServiceResponseDTO<AuthenticateResponseDto> | |||
| { | |||
| 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<AuthenticateResponseDto> | |||
| { | |||
| Data = data | |||
| }; | |||
| } | |||
| private async Task<string> 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(); | |||
| return writedToken; | |||
| } | |||
| public async Task<RefreshTokenResultDto> 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); | |||
| return new RefreshTokenResultDto | |||
| { | |||
| Data = new AuthenticateResponseDto | |||
| { | |||
| Id = userk.Id, | |||
| FirstName = userk.FirstName, | |||
| LastName = userk.LastName, | |||
| Username = userk.UserName, | |||
| Token = model.Token, | |||
| RefreshToken = model.RefreshToken | |||
| } | |||
| }; | |||
| } | |||
| public async Task<ServiceResponseDTO<string>> DeleteRefreshToken(int userId) | |||
| { | |||
| var refreshToken = await _databaseContext.RefreshTokens.Where(r => r.UserId == userId).FirstOrDefaultAsync(); | |||
| if (refreshToken is null) | |||
| return new ServiceResponseDTO<string> | |||
| { | |||
| 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<string> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Problem with saving changes into database" | |||
| }; | |||
| return new ServiceResponseDTO<string> | |||
| { | |||
| 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<RefreshToken?> 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<ServiceResponseDTO<object>> GetEmailConfirmationUrlAsync(string email) | |||
| { | |||
| var user = await _userManager.FindByEmailAsync(email); | |||
| if (user == null) | |||
| { | |||
| return new ServiceResponseDTO<object> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Email did not find." | |||
| }; | |||
| } | |||
| var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); | |||
| token = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(token)); | |||
| await _emailer.SendEmailAndWriteToDbAsync(email, "Reset password", renderHTMLtoString($"{_frontEndSettings.BaseUrl}/reset-password?token={token}&email={email}"), isHtml: true); | |||
| user.PasswordResetToken = token; | |||
| await _databaseContext.SaveChangesAsync(); | |||
| return new ServiceResponseDTO<object> | |||
| { | |||
| Data = new { code = token, email = email } | |||
| }; | |||
| } | |||
| private string renderHTMLtoString(string url) | |||
| { | |||
| return "<div style=\"font-family: sans-serif\">" + | |||
| "<div style=\"font-family: sans-serif;text-align: center;\">" + | |||
| "<h2 style=\"color: #017397;\">HR Center Password Reset</h2>" + | |||
| "<p style=\"font-size: 20px\">" + | |||
| "To reset your HR Center password, please click on the button below." + | |||
| "</p>" + | |||
| "<a style = \"color: white;text-decoration:none;background-color: #017397;cursor: pointer;font-size: 20px;width: 220px;text-align: center;border-radius: 5px;padding: 5px 15px;height: 25px;\" " + | |||
| $"href=\"{url}\">" + | |||
| " RESET PASSWORD" + | |||
| "</a>" + | |||
| "<p style = \"font-size: 12px; margin-top: 25px;\" >" + | |||
| "Please do not reply to this email.This message was sent from a notification-only address that is not monitored." + | |||
| "</p>" + | |||
| "</div>" + | |||
| "</div>"; | |||
| } | |||
| public async Task<ServiceResponseDTO<object>> PasswordResetAsync(string email, string code, string password) | |||
| { | |||
| var user = await _userManager.FindByEmailAsync(email); | |||
| if (user == null) | |||
| { | |||
| return new ServiceResponseDTO<object> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Email did not find." | |||
| }; | |||
| } | |||
| // FOR SOME REASON USERMANAGER.RESETPASSWORDASYNC returns InvalidToken. In future change this | |||
| //var passwordResetToken = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)); | |||
| //IdentityResult resetResult = await _userManager.ResetPasswordAsync(user, passwordResetToken, password); | |||
| //if (resetResult.Succeeded) | |||
| await _userManager.RemovePasswordAsync(user); | |||
| await _userManager.AddPasswordAsync(user, password); | |||
| if(user.PasswordResetToken == code) | |||
| { | |||
| if (await _userManager.IsLockedOutAsync(user)) | |||
| { | |||
| await _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow); | |||
| } | |||
| return new ServiceResponseDTO<object> { Data = true }; | |||
| } | |||
| //var errors = resetResult.Errors.Select(x => x.Description); | |||
| return new ServiceResponseDTO<object> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Invalid reset password token" | |||
| }; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,5 +1,6 @@ | |||
| global using Diligent.WebAPI.Business.Services.Interfaces; | |||
| global using Diligent.WebAPI.Business.Settings; | |||
| global using Diligent.WebAPI.Business.Helper; | |||
| global using Diligent.WebAPI.Data; | |||
| global using Diligent.WebAPI.Data.Entities; | |||
| @@ -0,0 +1,85 @@ | |||
| namespace Diligent.WebAPI.Host.Controllers.V1 | |||
| { | |||
| [ApiVersion("1.0")] | |||
| [Route("v{version:apiVersion}/authentications")] | |||
| [ApiController] | |||
| public class AuthenticationsController : ControllerBase | |||
| { | |||
| private readonly IAuthenticationService _service; | |||
| public AuthenticationsController(IAuthenticationService service) | |||
| { | |||
| _service = service; | |||
| } | |||
| [HttpGet("ForgotPassword")] | |||
| public async Task<IActionResult> ForgotPassword(string email) | |||
| { | |||
| var response = await _service.GetEmailConfirmationUrlAsync(email); | |||
| if (response.IsError is true) | |||
| return BadRequest(new { message = response.ErrorMessage }); | |||
| return Ok(response.Data); | |||
| } | |||
| [HttpPost("RessetPassword")] | |||
| public async Task<IActionResult> ResetPassword([FromBody] ResetPasswordModel model) | |||
| { | |||
| var response = await _service.PasswordResetAsync(email: model.Email, code: model.Code, password: model.Password); | |||
| if (response.IsError is true) | |||
| return BadRequest(new { message = response.ErrorMessage }); | |||
| return Ok(response.Data); | |||
| } | |||
| [HttpPost("authenticate")] | |||
| public async Task<IActionResult> Authenticate([FromBody] AuthenticateRequestDto model) | |||
| { | |||
| var response = await _service.Authenticate(model); | |||
| if (response.IsError is true) | |||
| return BadRequest(new { message = response.ErrorMessage }); | |||
| return Ok(response.Data); | |||
| } | |||
| [HttpPost("refresh")] | |||
| public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequestDto model) | |||
| { | |||
| var response = await _service.RefreshTokenAsync(model); | |||
| if (response.Error != null) | |||
| { | |||
| return BadRequest(new AuthFailedResponse { Error = response.Error }); | |||
| } | |||
| return Ok(response); | |||
| } | |||
| [HttpPost("logout")] | |||
| public async Task<IActionResult> Logout(int userId) | |||
| { | |||
| var response = await _service.DeleteRefreshToken(userId); | |||
| if (response.IsError) | |||
| { | |||
| return BadRequest(new { message = response.ErrorMessage }); | |||
| } | |||
| return Ok(); | |||
| } | |||
| [HttpPost("authenticateGoogle")] | |||
| public async Task<IActionResult> GoogleLogin(GoogleApiModel model) | |||
| { | |||
| var response = await _service.Authenticate(model); | |||
| if (response.IsError is true) | |||
| return BadRequest(new { message = response.ErrorMessage }); | |||
| return Ok(response.Data); | |||
| } | |||
| } | |||
| } | |||
| @@ -10,13 +10,11 @@ namespace Diligent.WebAPI.Host.Controllers.V1 | |||
| { | |||
| private readonly IUserService _userService; | |||
| private readonly IMapper _mapper; | |||
| private readonly IEmailer _emailer; | |||
| public UsersController(IUserService userService, IEmailer emailer, IMapper mapper) | |||
| public UsersController(IUserService userService, IMapper mapper) | |||
| { | |||
| _userService = userService; | |||
| _mapper = mapper; | |||
| _emailer = emailer; | |||
| } | |||
| [Authorize] | |||
| @@ -26,19 +24,7 @@ namespace Diligent.WebAPI.Host.Controllers.V1 | |||
| return Ok(_mapper.Map<IEnumerable<User?>, IEnumerable<UserResponseDTO>>(await _userService.GetAll())); | |||
| } | |||
| [HttpGet("ForgotPassword")] | |||
| public async Task<IActionResult> ForgotPassword(string email) | |||
| { | |||
| var result = await _userService.GetEmailConfirmationUrlAsync(email); | |||
| return Ok(result); | |||
| } | |||
| [HttpPost("RessetPassword")] | |||
| public async Task<IActionResult> ResetPassword([FromBody]ResetPasswordModel model) | |||
| { | |||
| var result = await _userService.PasswordResetAsync(email:model.Email,code: model.Code,password: model.Password); | |||
| return Ok(result); | |||
| } | |||
| [Authorize] | |||
| [HttpPost] | |||
| public async Task<IActionResult> CreateUser([FromBody] CreateUserRequestDto model) | |||
| { | |||
| @@ -46,53 +32,6 @@ namespace Diligent.WebAPI.Host.Controllers.V1 | |||
| return Ok(); | |||
| } | |||
| [HttpPost("authenticate")] | |||
| public async Task<IActionResult> Authenticate([FromBody] AuthenticateRequestDto model) | |||
| { | |||
| var response = await _userService.Authenticate(model); | |||
| if (response.IsError is true) | |||
| return BadRequest(new { message = response.ErrorMessage }); | |||
| return Ok(response.Data); | |||
| } | |||
| [HttpPost("refresh")] | |||
| public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequestDto model) | |||
| { | |||
| var response = await _userService.RefreshTokenAsync(model); | |||
| if (response.Error != null) | |||
| { | |||
| return BadRequest(new AuthFailedResponse { Error = response.Error }); | |||
| } | |||
| return Ok(response); | |||
| } | |||
| [HttpPost("logout")] | |||
| public async Task<IActionResult> Logout(int userId) | |||
| { | |||
| var response = await _userService.DeleteRefreshToken(userId); | |||
| if (response.IsError) | |||
| { | |||
| return BadRequest(new { message = response.ErrorMessage }); | |||
| } | |||
| return Ok(); | |||
| } | |||
| [HttpPost("authenticateGoogle")] | |||
| public async Task<IActionResult> GoogleLogin(GoogleApiModel model) | |||
| { | |||
| var response = await _userService.Authenticate(model); | |||
| if (response.IsError is true) | |||
| return BadRequest(new { message = response.ErrorMessage }); | |||
| return Ok(response.Data); | |||
| } | |||
| } | |||
| } | |||
| @@ -18,16 +18,5 @@ | |||
| { | |||
| return Ok("Hello from protected route"); | |||
| } | |||
| [HttpPost("authenticate")] | |||
| public IActionResult Authenticate([FromBody] AuthenticateRequestDto model) | |||
| { | |||
| var response = _userService.Authenticate(model); | |||
| if (response == null) | |||
| return BadRequest(new { message = "Username or password is incorrect" }); | |||
| return Ok(response); | |||
| } | |||
| } | |||
| } | |||
| @@ -15,6 +15,7 @@ | |||
| services.AddScoped<IInsurersService, InsurersService>(); | |||
| services.AddScoped<IEmailer, Emailer>(); | |||
| services.AddScoped<IHttpClientService, HttpClientService>(); | |||
| services.AddScoped<IInsuranceCompaniesService, InsuranceCompaniesService>(); | |||
| services.AddScoped<IInsurancePoliciesService, InsurancePoliciesService>(); | |||
| services.AddScoped<IWebhookSubscriptionService, WebhookSubscriptionService>(); | |||
| @@ -22,6 +23,7 @@ | |||
| services.AddScoped<IWebhookPublisherService, WebhookPublisherService>(); | |||
| services.AddScoped<IWebhookPublisherService, WebhookPublisherService>(); | |||
| services.AddScoped<IApplicantService, ApplicantService>(); | |||
| services.AddScoped<IAuthenticationService, AuthenticationService>(); | |||
| services.AddScoped<IAdService, AdService>(); | |||
| services.AddScoped<ITechnologyService, TechnologyService>(); | |||
| } | |||
| @@ -11,17 +11,17 @@ | |||
| _authSettings = authSettings.Value; | |||
| } | |||
| public async Task Invoke(HttpContext context, IUserService userService) | |||
| public async Task Invoke(HttpContext context, IAuthenticationService authService, IUserService userService) | |||
| { | |||
| var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); | |||
| if (token != null) | |||
| await AttachUserToContext(context, userService, token); | |||
| await AttachUserToContext(context, authService, userService, token); | |||
| await _next(context); | |||
| } | |||
| private async Task AttachUserToContext(HttpContext context, IUserService userService, string token) | |||
| private async Task AttachUserToContext(HttpContext context, IAuthenticationService authService,IUserService userService, string token) | |||
| { | |||
| try | |||
| { | |||
| @@ -45,7 +45,7 @@ | |||
| // attach user to context on successful jwt validation | |||
| context.Items["User"] = await userService.GetById(userId); | |||
| await UpdateRefreshToken(context, userService, userId); | |||
| await UpdateRefreshToken(context, authService, userId); | |||
| } | |||
| catch | |||
| { | |||
| @@ -54,17 +54,17 @@ | |||
| } | |||
| } | |||
| private async Task UpdateRefreshToken(HttpContext context, IUserService userService, int userId) | |||
| private async Task UpdateRefreshToken(HttpContext context, IAuthenticationService service, int userId) | |||
| { | |||
| var refreshToken = await userService.GetRefreshTokenByUserId(userId); | |||
| var refreshToken = await service.GetRefreshTokenByUserId(userId); | |||
| if (refreshToken == null) | |||
| return; | |||
| refreshToken.ExpiryDate = DateTime.UtcNow.AddMinutes(30); | |||
| await userService.UpdateRefreshToken(refreshToken); | |||
| await service.UpdateRefreshToken(refreshToken); | |||
| } | |||
| } | |||
| } | |||