195 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			195 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| 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 {
 | |
|     /// <summary>
 | |
|     /// 
 | |
|     /// </summary>
 | |
|     /// <param name="user"></param>
 | |
|     /// <param name="token"></param>
 | |
|     /// <returns></returns>
 | |
|     IDomainResult Authenticate(UserDocument user, string token);
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /// <summary>
 | |
|   /// 
 | |
|   /// </summary>
 | |
|   public interface IAccountService {
 | |
|     /// <summary>
 | |
|     /// 
 | |
|     /// </summary>
 | |
|     /// <param name="requestData"></param>
 | |
|     /// <returns></returns>
 | |
|     (string?, IDomainResult) Authenticate(AuthenticationRequestModel requestData);
 | |
| 
 | |
|     (Guid?, IDomainResult) PasswordChange(UserDocument user, string newPassword);
 | |
|   }
 | |
| 
 | |
|   /// <summary>
 | |
|   /// 
 | |
|   /// </summary>
 | |
|   public class AccountService : ServiceBase<AccountService>, IAccountService, IAccountPolicyService {
 | |
| 
 | |
|     private readonly IAesKey? _aesKey;
 | |
|     private readonly IUserDataProvider _userDataProvider;
 | |
|     private readonly IJWTService _jwtService;
 | |
| 
 | |
|     /// <summary>
 | |
|     /// 
 | |
|     /// </summary>
 | |
|     /// <param name="logger"></param>
 | |
|     /// <param name="options"></param>
 | |
|     /// <param name="userDataProvider"></param>
 | |
|     /// <param name="jwtService"></param>
 | |
|     public AccountService(
 | |
|       ILogger<AccountService> logger,
 | |
|       IOptions<Configuration> options,
 | |
|       IUserDataProvider userDataProvider,
 | |
|       IJWTService jwtService
 | |
|     ) : base(logger) {
 | |
|       _aesKey = options.Value.JwtTokenEncryption;
 | |
|       _userDataProvider = userDataProvider;
 | |
|       _jwtService = jwtService;
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /// <summary>
 | |
|     /// 
 | |
|     /// </summary>
 | |
|     /// <param name="requestData"></param>
 | |
|     /// <returns></returns>
 | |
|     public (string?, IDomainResult) Authenticate(AuthenticationRequestModel requestData) {
 | |
| 
 | |
|       if (_aesKey?.IV == null || _aesKey?.Key == null)
 | |
|         return IDomainResult.Failed<string?>("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<string?>("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<string?>();
 | |
| 
 | |
|       // 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<Token>();
 | |
| 
 | |
|         return IDomainResult.Failed<string?>("Password is expired, create new password.");
 | |
|       }
 | |
| 
 | |
|       // Creating JWT token
 | |
|       var claims = new List<KeyValuePair<string, string>> {
 | |
|         new KeyValuePair<string, string>("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<string, string>("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<string?>();
 | |
| 
 | |
|       return IDomainResult.Success(token);
 | |
|     }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// 
 | |
|     /// </summary>
 | |
|     /// <param name="user"></param>
 | |
|     /// <param name="token"></param>
 | |
|     /// <returns></returns>
 | |
|     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();
 | |
|     }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Passing the Username in the request body is a more secure alternative to passing it as a GET param
 | |
|     /// </summary>
 | |
|     /// <param name="requestData"></param>
 | |
|     /// <returns></returns>
 | |
|     /// <exception cref="NotImplementedException"></exception>
 | |
|     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();
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// 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
 | |
|     /// </summary>
 | |
|     /// <param name="requestData"></param>
 | |
|     /// <returns></returns>
 | |
|     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);
 | |
|     }
 | |
|   }
 | |
| }
 |