using Newtonsoft.Json; using System.Net; namespace Diligent.WebAPI.Business.Services { public class UserService : IUserService { private readonly AuthorizationSettings _authSettings; private readonly UserManager _userManager; private readonly IMapper _mapper; private readonly DatabaseContext _databaseContext; public UserService(IOptions authSettings, UserManager userManager, IMapper mapper, DatabaseContext databaseContext) { _authSettings = authSettings.Value; _userManager = userManager; _mapper = mapper; _databaseContext = databaseContext; } public async Task> GetAll() => await _userManager.Users.ToListAsync(); public async Task GetById(int id) => await _userManager.FindByIdAsync(id.ToString()); public async Task CreateUser(CreateUserRequestDto model) { var user = _mapper.Map(model); await _userManager.CreateAsync(user, model.Password); } private const string GoogleApiTokenInfoUrl = "https://www.googleapis.com/oauth2/v3/tokeninfo?id_token={0}"; private string[] SupportedClientsIds = { "734219382849-nvnulsu7ibfl4bk3n164bgb7c1h5dgca.apps.googleusercontent.com" }; 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(response); if (!SupportedClientsIds.Contains(googleApiTokenInfo.aud)) { return false; } return true; //return new //{ // Email = googleApiTokenInfo.email, // FirstName = googleApiTokenInfo.given_name, // LastName = googleApiTokenInfo.family_name, // Locale = googleApiTokenInfo.locale, // Name = googleApiTokenInfo.name, // ProviderUserId = googleApiTokenInfo.sub //}; } public async Task> Authenticate(AuthenticateRequestDto model) { var user = await _userManager.FindByNameAsync(model.Username); // return null if user not found if (user == null) { return new ServiceResponseDTO { 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 { IsError = true, ErrorMessage = "Password is not correct" }; } return await GenerateToken(user); } public async Task> Authenticate(GoogleApiModel model) { if (!IsTokenValid(model.Token)) { return new ServiceResponseDTO { 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 { IsError = true, ErrorMessage = $"User with email {model.User.email} does not exist in database" }; } return await GenerateToken(user); } 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, Guid.NewGuid().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 = token.Id, UserId = user.Id, User = user, CreationDate = DateTime.UtcNow, ExpiryDate = DateTime.UtcNow.AddMinutes(_authSettings.JwtRefreshExpiredTime) }; var existRefreshToken = await _databaseContext.RefreshTokens.Where(x => x.Id == 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 RefreshTokenAsync(RefreshTokenRequestDto model) { var validatedToken = GetPrincipalFromToken(model.Token); if (validatedToken == null) { return new RefreshTokenResultDto { Error = "Invalid 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) .AddSeconds(expiryDateUnix); if (expiryDateTimeUtc > DateTime.UtcNow) { return new RefreshTokenResultDto { Error = "This token hasn't expired yet" }; } var jti = validatedToken.Claims.Single(x => x.Type == JwtRegisteredClaimNames.Jti).Value; var storedRefreshToken = await _databaseContext.RefreshTokens.SingleOrDefaultAsync(x => x.Token == model.RefreshToken); if (storedRefreshToken == null) { return new RefreshTokenResultDto { Error = "This refresh token does not exist" }; } 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.Used) { return new RefreshTokenResultDto { Error = "This refresh token has been used" }; } if (storedRefreshToken.JwtId != jti) { return new RefreshTokenResultDto { Error = "This refresh token does not match this JWT" }; } storedRefreshToken.Used = true; _databaseContext.RefreshTokens.Update(storedRefreshToken); await _databaseContext.SaveChangesAsync(); var user = await _userManager.FindByIdAsync(validatedToken.Claims.Single(x => x.Type == "id").Value); var token = await GenerateJwtToken(user); return new RefreshTokenResultDto { Token = token }; } private ClaimsPrincipal GetPrincipalFromToken(string token) { 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 = true, // 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 ex) { return null; } } private 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(); } } }