diff --git a/db/DML/user.json b/db/DML/user.json index e67c98e..db731e8 100644 --- a/db/DML/user.json +++ b/db/DML/user.json @@ -7,25 +7,26 @@ "created": { "$date": "2022-01-01T00:00:00.000Z" }, - "nickaName": "hailstrike", + "nickaName": "admin", "passwords": { - "pwHash": "", - "pwSalt": "", - "created": { - "$date": "2022-01-01T00:00:00.000Z" + "password": { + "hash": "pznndK3nv9bftf/qQxqBy4VjH7Ow9vx2Kd6376oJuqQ=", + "salt": "gkEl1zxGJSLue262mUu5VA==", + "created": { + "$date": "2022-01-01T00:00:00.000Z" + } }, + "expiration": { - "$date": "2023-01-01T00:00:00.000Z" + "enabled": false, + "days": "180" }, "expired": [ { - "pwHash": "", - "pwSalt": "", + "hash": "", + "salt": "", "created": { "$date": "2022-01-01T00:00:00.000Z" - }, - "expiration": { - "$date": "2023-01-01T00:00:00.000Z" } } ] @@ -35,7 +36,7 @@ "contacts": [ { "type": 0, - "value": "john.doe@maks-it.com", + "value": "john.doe@contoso.com", "confirmed": false }, { diff --git a/webapi/Core/DomainObjects/Documents/User.cs b/webapi/Core/DomainObjects/Documents/User.cs index 94b55f8..171e3f9 100644 --- a/webapi/Core/DomainObjects/Documents/User.cs +++ b/webapi/Core/DomainObjects/Documents/User.cs @@ -21,7 +21,7 @@ namespace Core.DomainObjects { public Address ShippingAddress { get; set; } - public List Tokens { get; set; } + public List Tokens { get; set; } public override int GetHashCode() { throw new NotImplementedException(); diff --git a/webapi/Core/DomainObjects/Password.cs b/webapi/Core/DomainObjects/Password.cs new file mode 100644 index 0000000..377ebba --- /dev/null +++ b/webapi/Core/DomainObjects/Password.cs @@ -0,0 +1,24 @@ +using Core.Abstractions.DomainObjects; + +namespace Core.DomainObjects { + public class Password : DomainObjectBase { + + public string Hash { get; set; } + + public string Salt { get; set; } + + public DateTime Created { get; set; } + + public Password(string hash, string salt, DateTime? created) { + Hash = hash; + Salt = salt; + Created = created ?? DateTime.UtcNow; + } + + public Password Prototype() => new (Hash, Salt, Created); + + public override int GetHashCode() { + throw new NotImplementedException(); + } + } +} diff --git a/webapi/Core/DomainObjects/PasswordExpiration.cs b/webapi/Core/DomainObjects/PasswordExpiration.cs new file mode 100644 index 0000000..9d5d414 --- /dev/null +++ b/webapi/Core/DomainObjects/PasswordExpiration.cs @@ -0,0 +1,17 @@ +using Core.Abstractions.DomainObjects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Core.DomainObjects { + public class PasswordExpiration : DomainObjectBase { + public bool Enabled { get; set; } + public int Days { get; set; } + + public override int GetHashCode() { + throw new NotImplementedException(); + } + } +} diff --git a/webapi/Core/DomainObjects/Passwords.cs b/webapi/Core/DomainObjects/Passwords.cs index 96248ae..52ebeae 100644 --- a/webapi/Core/DomainObjects/Passwords.cs +++ b/webapi/Core/DomainObjects/Passwords.cs @@ -6,17 +6,14 @@ using System.Text; using System.Threading.Tasks; namespace Core.DomainObjects { + public class Passwords : DomainObjectBase { - public string PwHash { get; set; } + public Password? Password { get; set; } - public string PwSalt { get; set; } + public PasswordExpiration Expiration { get; set; } - public DateTime Created { get; set; } - - public DateTime? Expiration { get; set; } - - public List Expired { get; set; } + public List Expired { get; set; } public override int GetHashCode() { throw new NotImplementedException(); diff --git a/webapi/Core/DomainObjects/Token.cs b/webapi/Core/DomainObjects/Token.cs new file mode 100644 index 0000000..c4cfb31 --- /dev/null +++ b/webapi/Core/DomainObjects/Token.cs @@ -0,0 +1,20 @@ +using Core.Abstractions.DomainObjects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Core.DomainObjects { + public class Token : DomainObjectBase { + + public string Value { get; set; } + + public DateTime Created { get; set; } + public DateTime Expires { get; set; } + + public override int GetHashCode() { + throw new NotImplementedException(); + } + } +} diff --git a/webapi/DataProviders/Collections/ShopCartDataProvider.cs b/webapi/DataProviders/Collections/ShopCartDataProvider.cs index e271470..6a4cb53 100644 --- a/webapi/DataProviders/Collections/ShopCartDataProvider.cs +++ b/webapi/DataProviders/Collections/ShopCartDataProvider.cs @@ -21,6 +21,7 @@ namespace DataProviders.Collections { public class ShopCartDataProvider : CollectionDataProviderBase, IShopCartDataProvider { private const string _collectionName = "shopcart"; + public ShopCartDataProvider( ILogger> logger, IMongoClient client, diff --git a/webapi/DataProviders/Collections/UserDataProvider.cs b/webapi/DataProviders/Collections/UserDataProvider.cs index 7d3fde6..8287bb0 100644 --- a/webapi/DataProviders/Collections/UserDataProvider.cs +++ b/webapi/DataProviders/Collections/UserDataProvider.cs @@ -1,4 +1,10 @@ -using System; +using Core.DomainObjects; +using DataProviders.Abstractions; +using DomainResults.Common; +using Microsoft.Extensions.Logging; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -7,11 +13,38 @@ using System.Threading.Tasks; namespace DataProviders.Collections { public interface IUserDataProvider { + (User?, IDomainResult) Get(Guid userId); + (User?, IDomainResult) GetByNickName(string nickName); } - internal class UserDataProvider : IUserDataProvider { + public class UserDataProvider : CollectionDataProviderBase, IUserDataProvider { + private const string _collectionName = "users"; + public UserDataProvider( + ILogger logger, + IMongoClient client, + IIdGenerator idGenerator, + ISessionService sessionService) : base(logger, client, idGenerator, sessionService) { + } + + public (User?, IDomainResult) Get( Guid userId) { + var (list, result) = GetWithPredicate(x => x.Id == userId, _collectionName); + + if (!result.IsSuccess || list == null) + return (null, result); + + return (list.First(), result); + } + + public (User?, IDomainResult) GetByNickName(string nickName) { + var (list, result) = GetWithPredicate(x => x.NickName == nickName, _collectionName); + + if (!result.IsSuccess || list == null) + return (null, result); + + return (list.First(), result); + } } } diff --git a/webapi/Services/JWTService/IJWTServiceConfig.cs b/webapi/Services/JWTService/IJWTServiceConfig.cs index 2ccee90..a61d041 100644 --- a/webapi/Services/JWTService/IJWTServiceConfig.cs +++ b/webapi/Services/JWTService/IJWTServiceConfig.cs @@ -7,11 +7,11 @@ namespace JWTService { public interface IJwtConfig { public string? Secret { get; set; } - public double? Expires { get; set; } + public int? Expires { get; set; } } public class JwtConfig : IJwtConfig { public string? Secret { get; set; } - public double? Expires { get; set; } + public int? Expires { get; set; } } } diff --git a/webapi/Services/JWTService/JWTService.cs b/webapi/Services/JWTService/JWTService.cs index b6099db..099885d 100644 --- a/webapi/Services/JWTService/JWTService.cs +++ b/webapi/Services/JWTService/JWTService.cs @@ -1,87 +1,66 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; +using DomainResults.Common; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; namespace JWTService { public interface IJWTService { - string CreateJwtToken(); - JwtSecurityToken ReadJwtToken(string token); + + string CreateJwtToken(DateTime expires, List>? claims); + (List>?, IDomainResult) JwtTokenClaims(string token); } + public class JWTService : IJWTService { private readonly ILogger _logger; + private readonly IJwtConfig _configuration; - private readonly JwtSecurityTokenHandler _tokenHandler; - private readonly IJwtConfig _serviceConfig; - - /// - /// - /// - /// public JWTService( ILogger logger, - IJwtConfig serviceConfig - + IJwtConfig configuration ) { _logger = logger; - _serviceConfig = serviceConfig; - _tokenHandler = new JwtSecurityTokenHandler(); + _configuration = configuration; } - public string? CreateJwtToken() { - if (_serviceConfig.Secret == null) - return null; + public string CreateJwtToken(DateTime expires, List>? claims) => + CreateJwtToken(_configuration.Secret, expires, claims); - if (_serviceConfig.Expires == null) - return null; - - - var key = Convert.FromBase64String(_serviceConfig.Secret); + public string CreateJwtToken(string secret, DateTime expires, List>? claims) { // add roles to claims identity from database - var claims = new List() {}; + var tokenClaims = new List(); + if (claims != null) + foreach (var claim in claims) + tokenClaims.Add(new Claim(claim.Key, claim.Value)); - var token = _tokenHandler.CreateToken(new SecurityTokenDescriptor { + var tokenHandler = new JwtSecurityTokenHandler(); + + var securityToken = tokenHandler.CreateToken(new SecurityTokenDescriptor { IssuedAt = DateTime.UtcNow, - Subject = new ClaimsIdentity(claims), - Expires = DateTime.UtcNow.AddDays(_serviceConfig.Expires.Value), - SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature), + Subject = new ClaimsIdentity(tokenClaims), + Expires = expires, + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Convert.FromBase64String(secret)), SecurityAlgorithms.HmacSha512Signature), }); + _logger.LogInformation($"Creted new JWT {securityToken}"); - return _tokenHandler.WriteToken(token); + return tokenHandler.WriteToken(securityToken); } - - //public string CreateJwtToken(IEnumerable issuer, DateTime expires, string userId, string userEmail, string userName, IEnumerable userRoles) { - // var key = Convert.FromBase64String(_serviceConfig.Secret); + public (List>?, IDomainResult) JwtTokenClaims(string token) { - // // add roles to claims identity from database - // var claims = new List() { - // new Claim(ClaimTypes.Actor, userId), - // new Claim(ClaimTypes.Email, userEmail), - // new Claim(ClaimTypes.NameIdentifier, userName), - // // new Claim(ClaimTypes.Webpage, issuer) - // }; + var securityToken = new JwtSecurityTokenHandler().ReadToken(token) as JwtSecurityToken; + var claims = securityToken?.Claims?.Select(x => new KeyValuePair(x.Type, x.Value)); - // foreach (var role in userRoles) - // claims.Add(new Claim(ClaimTypes.Role, role)); + if (claims == null) + return IDomainResult.Failed>?>(); - // foreach (var iss in issuer) - // claims.Add(new Claim(ClaimTypes.Webpage, iss)); - - // var token = _tokenHandler.CreateToken(new SecurityTokenDescriptor { - // IssuedAt = DateTime.UtcNow, - // Subject = new ClaimsIdentity(claims), - // Expires = expires, - // SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature), - // }); - - // return _tokenHandler.WriteToken(token); - //} - - public JwtSecurityToken ReadJwtToken(string token) => _tokenHandler.ReadJwtToken(token); + return claims.Count() > 0 + ? IDomainResult.Success(claims.ToList()) + : IDomainResult.NotFound>?>(); + } } } diff --git a/webapi/WeatherForecast/Controllers/PasswordController.cs b/webapi/WeatherForecast/Controllers/PasswordController.cs index 410978c..7ae8ecd 100644 --- a/webapi/WeatherForecast/Controllers/PasswordController.cs +++ b/webapi/WeatherForecast/Controllers/PasswordController.cs @@ -17,6 +17,10 @@ public class PasswordController : ControllerBase { private readonly IPasswordService _passwordService; + /// + /// + /// + /// public PasswordController( IPasswordService passwordService ) { diff --git a/webapi/WeatherForecast/Controllers/UserController.cs b/webapi/WeatherForecast/Controllers/UserController.cs new file mode 100644 index 0000000..dce3c2d --- /dev/null +++ b/webapi/WeatherForecast/Controllers/UserController.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace WeatherForecast.Controllers; + +/// +/// +/// +[Authorize] +[ApiController] +[Route("api/[controller]")] +public class UserController : ControllerBase { + + /// + /// + /// + public UserController() { } +} diff --git a/webapi/WeatherForecast/Services/AutheticationService.cs b/webapi/WeatherForecast/Services/AutheticationService.cs index 866c617..eefcd78 100644 --- a/webapi/WeatherForecast/Services/AutheticationService.cs +++ b/webapi/WeatherForecast/Services/AutheticationService.cs @@ -1,6 +1,9 @@ using Core.Abstractions; +using Core.DomainObjects; using DataProviders.Collections; using DomainResults.Common; +using ExtensionMethods; +using HashService; using JWTService; using WeatherForecast.Models.Requests; @@ -26,6 +29,7 @@ namespace WeatherForecast.Services { public class AutheticationService : ServiceBase, IAuthenticationService { private readonly IUserDataProvider _userDataProvider; + private readonly IHashService _hashService; private readonly IJWTService _jwtService; /// @@ -33,13 +37,16 @@ namespace WeatherForecast.Services { /// /// /// + /// /// public AutheticationService ( ILogger logger, IUserDataProvider userDataProvider, + IHashService hashService, IJWTService jwtService ) : base(logger) { _userDataProvider = userDataProvider; + _hashService = hashService; _jwtService = jwtService; } @@ -49,18 +56,77 @@ namespace WeatherForecast.Services { /// /// /// - public (string?, IDomainResult) Post (Guid siteId, AuthenticationRequestModel requestData) { - try { - var token = _jwtService.CreateJwtToken(); + public (string?, IDomainResult) Post(Guid siteId, AuthenticationRequestModel requestData) { + var opId = Guid.NewGuid(); + + // Retrieve user from database by userName + var (user, getUserResult) = _userDataProvider.GetByNickName(requestData.Username); + if (!getUserResult.IsSuccess || user == null) + return IDomainResult.NotFound(); + + if (user.Passwords.Password == null) + return IDomainResult.Failed($"Opid = [{opId}] 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; - return token != null - ? IDomainResult.Success(token) - : IDomainResult.Failed(); - } - catch (Exception ex) { - _logger.LogError("Unhandled exception", ex); return IDomainResult.Failed(); } + + // Creating JWT token + var claims = new List> { + new KeyValuePair("UserId", $"{user.Id}") + }; + + var created = DateTime.UtcNow; + var expires = created.AddDays(365); + + var token = _jwtService.CreateJwtToken(expires, claims); + + user.Tokens.Add(new Token { + Value = token, + Created = created, + Expires = expires, + }); + + return IDomainResult.Success(token); + } + + /// + /// + /// + /// + /// + public IDomainResult Get(string token) { + + #region Retrieve user id from token claim + var (claims, getClaimsResult) = _jwtService.JwtTokenClaims(token); + if (!getClaimsResult.IsSuccess || claims == null) + return IDomainResult.Failed(); + + var userId = claims.SingleOrDefault(x => x.Key == "UserId").Value.ToGuid(); + if(userId == Guid.Empty) + return IDomainResult.Failed(); + #endregion + + var (user, getUserRersult) = _userDataProvider.Get(userId); + if(!getUserRersult.IsSuccess || user == null) + return IDomainResult.Failed(); + + // remove expired tokens + user.Tokens = user.Tokens.Where(x => x.Expires < DateTime.UtcNow).ToList(); + + if (!user.Tokens.Any(x => x.Value == token)) + return IDomainResult.Failed(); + + return IDomainResult.Success(); } } } diff --git a/webapi/WeatherForecast/Services/UserService.cs b/webapi/WeatherForecast/Services/UserService.cs new file mode 100644 index 0000000..d76769a --- /dev/null +++ b/webapi/WeatherForecast/Services/UserService.cs @@ -0,0 +1,16 @@ +namespace WeatherForecast.Services { + + /// + /// + /// + public interface IUserService { + + } + + /// + /// + /// + public class UserService : IUserService { + + } +} diff --git a/webapi/WeatherForecast/Startup.cs b/webapi/WeatherForecast/Startup.cs index bda41b7..f020de4 100644 --- a/webapi/WeatherForecast/Startup.cs +++ b/webapi/WeatherForecast/Startup.cs @@ -84,6 +84,7 @@ namespace WeatherForecast { services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped();