using Core.Abstractions; using DomainObjects; using CryptoProvider; using DataProviders.Collections; using DomainObjects.Documents; using DomainResults.Common; using JWTService; using Microsoft.Extensions.Options; using WeatherForecast.Models.Account.Requests; namespace WeatherForecast.Services { public interface IAccountPolicyService { /// /// /// /// /// /// IDomainResult Authenticate(UserDocument user, string token); } /// /// /// public interface IAccountService { /// /// /// /// /// (string?, IDomainResult) Authenticate(AuthenticationRequestModel requestData); (Guid?, IDomainResult) PasswordChange(UserDocument user, string newPassword); } /// /// /// public class AccountService : ServiceBase, IAccountService, IAccountPolicyService { private readonly IAesKey? _aesKey; private readonly IUserDataProvider _userDataProvider; private readonly IJWTService _jwtService; /// /// /// /// /// /// /// public AccountService( ILogger logger, IOptions options, IUserDataProvider userDataProvider, IJWTService jwtService ) : base(logger) { _aesKey = options.Value.JwtTokenEncryption; _userDataProvider = userDataProvider; _jwtService = jwtService; } /// /// /// /// /// public (string?, IDomainResult) Authenticate(AuthenticationRequestModel requestData) { if (_aesKey?.IV == null || _aesKey?.Key == null) return IDomainResult.Failed("IV or Key are not set"); // Retrieve user from database by userName var (user, getUserResult) = _userDataProvider.GetByUsername(requestData.Username); if (!getUserResult.IsSuccess || user == null) return (null, getUserResult); if (user.Passwords.Password == null) return IDomainResult.Failed("Password is not set, create new password."); // Check provided password hash with the stored one var (salt, hash) = HashService.CreateSaltedHash(requestData.Password); if (!HashService.ValidateHash(requestData.Password, salt, hash)) return IDomainResult.Unauthorized(); // Check password expiration if enabled if (user.Passwords.Expiration.Enabled && DateTime.UtcNow > user.Passwords.Password.Created.AddDays(user.Passwords.Expiration.Days)) { user.Passwords.Expired.Add(user.Passwords.Password.Prototype()); user.Passwords.Password = null; user.Tokens = new List(); return IDomainResult.Failed("Password is expired, create new password."); } // Creating JWT token var claims = new List> { new KeyValuePair("unique_name", $"{user.Id}"), // (JWT ID): Unique identifier; can be used to prevent the JWT from being replayed (allows a token to be used only once) new KeyValuePair("jti", $"{Guid.NewGuid()}") }; var created = DateTime.UtcNow; var expires = created.AddDays(365); var token = _jwtService.CreateJwtToken(expires, claims); user.Tokens.Add(new Token { Value = AesService.EncryptString(_aesKey.IV, _aesKey.Key, token), Created = created, Expires = expires, }); var (_, usdateUserResult) = _userDataProvider.Update(user); if (!usdateUserResult.IsSuccess) return IDomainResult.Failed(); return IDomainResult.Success(token); } /// /// /// /// /// /// public IDomainResult Authenticate(UserDocument user, string token) { if (_aesKey?.IV == null || _aesKey?.Key == null) return IDomainResult.Failed("IV or Key are not set"); #region Tokens cleanup var userTokens = user.Tokens.Where(x => x.Expires > DateTime.UtcNow).OrderByDescending(x => x.Expires).Take(10).ToList(); if (user.Tokens.Count != userTokens.Count) { user.Tokens = userTokens; _userDataProvider.Update(user); } #endregion // Check if whitelisted if (!userTokens.Select(x => AesService.DecryptString(_aesKey.IV, _aesKey.Key, x.Value)).Any(x => string.Compare(x, token) == 0)) return IDomainResult.Unauthorized(); return IDomainResult.Success(); } /// /// Passing the Username in the request body is a more secure alternative to passing it as a GET param /// /// /// /// public IDomainResult PasswordRecovery(PasswordRecoveryRequestModel requestData) { var (user, getUserResult) = _userDataProvider.GetByUsername(requestData.Username); if (!getUserResult.IsSuccess || user == null) return getUserResult; // // send an email throw new NotImplementedException(); } /// /// When the form is submitted with the new password and the token as inputs the reset password process will take place. /// The form data will be sent with a PUT request again but this time including the token and we will replace the resource password with a new value /// /// /// public IDomainResult PasswordReset(PasswordResetRequestModel requestData) { var (salt, hash) = HashService.CreateSaltedHash(requestData.Password); return IDomainResult.Success(); } public (Guid?, IDomainResult) PasswordChange(UserDocument user, string newPassword) { user.SetPassword(newPassword); return IDomainResult.Success(user.Id); } } }