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);
|
|
}
|
|
}
|
|
}
|