diff --git a/.gitignore b/.gitignore index b24d71e..0944ee7 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,9 @@ Thumbs.db *.mov *.wmv +.vs +bin +obj +*.user +ManagerService/bin +ManagerService/obj diff --git a/Manager.Framework/Business/ProfileLogic.cs b/Manager.Framework/Business/ProfileLogic.cs new file mode 100644 index 0000000..4da450d --- /dev/null +++ b/Manager.Framework/Business/ProfileLogic.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Manager.Framework.Business +{ + public class ProfileLogic + { + private readonly ILogger _logger; + + public ProfileLogic(ILogger logger) + : base() + { + _logger = logger; + } + + public bool Authenticate(string email, string password) + { + if (string.IsNullOrWhiteSpace(email)) + { + _logger.LogError($"Authenticate error: No e-mail provided"); + throw new UnauthorizedAccessException("Authentication error"); + } + if (string.IsNullOrEmpty(password)) + { + _logger.LogError($"Authenticate error: No password provided"); + throw new UnauthorizedAccessException("Authentication error"); + } + + return true; + } + } +} diff --git a/Manager.Framework/Manager.Framework.csproj b/Manager.Framework/Manager.Framework.csproj new file mode 100644 index 0000000..7e43e1d --- /dev/null +++ b/Manager.Framework/Manager.Framework.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/Manager.Framework/Models/Exceptions/RequestException.cs b/Manager.Framework/Models/Exceptions/RequestException.cs new file mode 100644 index 0000000..933a8c3 --- /dev/null +++ b/Manager.Framework/Models/Exceptions/RequestException.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; + +namespace Manager.Framework.Models +{ + [System.Serializable] + public class RequestException : System.Exception + { + public int StatusCode { get; set; } + + public object Payload { get; set; } + + protected RequestException() { } + + public RequestException(int statusCode) : this() { StatusCode = statusCode; } + public RequestException(int statusCode, string message) : base(message) { StatusCode = statusCode; } + public RequestException(int statusCode, string message, System.Exception inner) : base(message, inner) { StatusCode = statusCode; } + protected RequestException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public string GetJson() + { + return JsonSerializer.Serialize(new { StatusCode, Message, Payload }); + } + } +} diff --git a/Manager.Interfaces/DTO/LoginDTO.cs b/Manager.Interfaces/DTO/LoginDTO.cs new file mode 100644 index 0000000..5e3cfc3 --- /dev/null +++ b/Manager.Interfaces/DTO/LoginDTO.cs @@ -0,0 +1,8 @@ +namespace Manager.Interfaces.DTO +{ + public class LoginDTO + { + public string Email { get; set; } + public string Password { get; set; } + } +} diff --git a/Manager.Interfaces/DTO/SwaggerTokenRequest.cs b/Manager.Interfaces/DTO/SwaggerTokenRequest.cs new file mode 100644 index 0000000..8873a9f --- /dev/null +++ b/Manager.Interfaces/DTO/SwaggerTokenRequest.cs @@ -0,0 +1,14 @@ +namespace Manager.Interfaces.DTO +{ + /// + /// Swagger test client authentication data + /// + public class SwaggerTokenRequest + { + public string grant_type { get; set; } + public string username { get; set; } + public string password { get; set; } + public string client_id { get; set; } + public string client_secret { get; set; } + } +} diff --git a/Manager.Interfaces/DTO/TokenDTO.cs b/Manager.Interfaces/DTO/TokenDTO.cs new file mode 100644 index 0000000..7a74060 --- /dev/null +++ b/Manager.Interfaces/DTO/TokenDTO.cs @@ -0,0 +1,14 @@ +using System; + +namespace Manager.Interfaces.DTO +{ + public class TokenDTO + { + public string access_token { get; set; } + public string refresh_token { get; set; } + public string scope { get; set; } + public string token_type { get; set; } + public int expires_in { get; set; } + public DateTimeOffset expiration { get; set; } + } +} diff --git a/Manager.Interfaces/DTO/UserDetailDTO.cs b/Manager.Interfaces/DTO/UserDetailDTO.cs new file mode 100644 index 0000000..cb8b5fa --- /dev/null +++ b/Manager.Interfaces/DTO/UserDetailDTO.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Manager.Interfaces.DTO +{ + public class UserDetailDTO + { + public string Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + } +} diff --git a/Manager.Interfaces/Manager.Interfaces.csproj b/Manager.Interfaces/Manager.Interfaces.csproj new file mode 100644 index 0000000..d66e047 --- /dev/null +++ b/Manager.Interfaces/Manager.Interfaces.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/Manager.Interfaces/Models/Policy.cs b/Manager.Interfaces/Models/Policy.cs new file mode 100644 index 0000000..679cfd2 --- /dev/null +++ b/Manager.Interfaces/Models/Policy.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Manager.Interfaces.Models +{ + public class Policy + { + public string Name { get; set; } + public string[] Claims { get; set; } + } +} diff --git a/Manager.Interfaces/Models/TokensSettings.cs b/Manager.Interfaces/Models/TokensSettings.cs new file mode 100644 index 0000000..a224db0 --- /dev/null +++ b/Manager.Interfaces/Models/TokensSettings.cs @@ -0,0 +1,18 @@ +namespace Manager.Interfaces.Models +{ + public class TokensSettings + { + /// + /// Application secret for tokens generation + /// + public string Secret { get; set; } + /// + /// Access token expiration in minutes + /// + public int AccessTokenExpiration { get; set; } = 30; + /// + /// Refresh token expiration in minutes + /// + public int RefreshTokenExpiration { get; set; } = 4 * 60; + } +} diff --git a/Manager.Interfaces/Models/User.cs b/Manager.Interfaces/Models/User.cs new file mode 100644 index 0000000..5998f1b --- /dev/null +++ b/Manager.Interfaces/Models/User.cs @@ -0,0 +1,53 @@ +using Manager.Interfaces.DTO; +using MongoDB.Bson.Serialization.Attributes; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Manager.Interfaces.Models +{ + /// + /// User Information + /// + public class User + { + [BsonId] + [BsonRepresentation(MongoDB.Bson.BsonType.ObjectId)] + public string Id { get; set; } + + [BsonElement("Email")] + [BsonRequired] + public string Email { get; set; } // UNIQUE !.. + + [BsonElement("Password")] + [BsonRequired] + public string Password { get; set; } + + [BsonElement("FirstName")] + [BsonRequired] + public string FirstName { get; set; } + + [BsonElement("LastName")] + [BsonRequired] + public string LastName { get; set; } + + [BsonElement("Token")] + [BsonRequired] + public string Token { get; set; } + + [BsonElement("DateCreation")] + public DateTime DateCreation { get; set; } + + public UserDetailDTO ToDTO() + { + return new UserDetailDTO() + { + Id = Id, + Email = Email, + FirstName = FirstName, + LastName = LastName, + }; + } + + } +} diff --git a/ManagerService.sln b/ManagerService.sln new file mode 100644 index 0000000..421c103 --- /dev/null +++ b/ManagerService.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30907.101 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagerService", "ManagerService\ManagerService.csproj", "{042E0BC4-8DCF-4EEC-8420-C71AA85D4D99}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Manager.Framework", "Manager.Framework\Manager.Framework.csproj", "{B9548312-650F-4538-85B8-8447F8BB2BD3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Manager.Interfaces", "Manager.Interfaces\Manager.Interfaces.csproj", "{0B2EDCA0-C813-4EAA-9215-E219AE884F7D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {042E0BC4-8DCF-4EEC-8420-C71AA85D4D99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {042E0BC4-8DCF-4EEC-8420-C71AA85D4D99}.Debug|Any CPU.Build.0 = Debug|Any CPU + {042E0BC4-8DCF-4EEC-8420-C71AA85D4D99}.Release|Any CPU.ActiveCfg = Release|Any CPU + {042E0BC4-8DCF-4EEC-8420-C71AA85D4D99}.Release|Any CPU.Build.0 = Release|Any CPU + {B9548312-650F-4538-85B8-8447F8BB2BD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9548312-650F-4538-85B8-8447F8BB2BD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9548312-650F-4538-85B8-8447F8BB2BD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9548312-650F-4538-85B8-8447F8BB2BD3}.Release|Any CPU.Build.0 = Release|Any CPU + {0B2EDCA0-C813-4EAA-9215-E219AE884F7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B2EDCA0-C813-4EAA-9215-E219AE884F7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B2EDCA0-C813-4EAA-9215-E219AE884F7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B2EDCA0-C813-4EAA-9215-E219AE884F7D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B478AB21-859A-4B1E-BF99-77B01959805A} + EndGlobalSection +EndGlobal diff --git a/ManagerService/Controllers/AuthenticationController.cs b/ManagerService/Controllers/AuthenticationController.cs new file mode 100644 index 0000000..78f8673 --- /dev/null +++ b/ManagerService/Controllers/AuthenticationController.cs @@ -0,0 +1,104 @@ +using Manager.Interfaces.DTO; +using Manager.Services; +using ManagerService.Service.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; +using NSwag.Annotations; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +namespace ManagerService.Service.Controllers +{ + /// + /// Authentication controller + /// + [ApiController, Route("api/[controller]")] + [Authorize] + [OpenApiTag("Authentication", Description = "Authentication management")] + public class AuthenticationController : ControllerBase + { + private readonly ILogger _logger; + private readonly TokensService _tokensService; + private readonly UserDatabaseService _UserDatabaseService; + + public AuthenticationController(ILogger logger, TokensService tokensService, UserDatabaseService UserDatabaseService) + { + _logger = logger; + _tokensService = tokensService; + _UserDatabaseService = UserDatabaseService; + } + + /// + /// Authenticate (business) + /// + /// user email + /// user password + /// Token descriptor + private ObjectResult Authenticate(string email, string password) + { + try + { + // For TEST + email = "test@email.be"; + password = "kljqsdkljqsd"; + var token = _tokensService.Authenticate(email.ToLower(), password); + + // Set user token ? + var user = _UserDatabaseService.GetByEmail(email.ToLower()); + + if (user == null) + throw new KeyNotFoundException("User not found"); + + return new OkObjectResult(token); + } + catch (UnauthorizedAccessException ex) + { + _logger?.LogError(ex, $"Authentication error for user '{email}': unauthorized access"); + return Unauthorized(ex); + } + catch (Exception ex) + { + _logger?.LogError(ex, $"Authenticate error for user '{email}'"); + return Problem($"Authenticate error for user '{email}': {ex.Message}"); + } + } + + /// + /// Authenticate with form parameters (used by Swagger test client) + /// + /// Swagger token request + /// Token descriptor + [AllowAnonymous] + [HttpPost("Token")] + [Consumes("application/x-www-form-urlencoded")] + [ProducesResponseType(typeof(TokenDTO), (int) HttpStatusCode.OK)] + [ProducesResponseType(typeof(string), (int) HttpStatusCode.Unauthorized)] + [ProducesResponseType(typeof(string), (int) HttpStatusCode.InternalServerError)] + public ObjectResult AuthenticateWithForm([FromForm] SwaggerTokenRequest tokenRequest) + { + return Authenticate(tokenRequest.username, tokenRequest.password); + } + + /// + /// Authenticate with Json parameters (used by most clients) + /// + /// Login DTO + /// Token descriptor + [AllowAnonymous] + [HttpPost("Authenticate")] + [Consumes("application/json")] + [ProducesResponseType(typeof(TokenDTO), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(string), (int)HttpStatusCode.Unauthorized)] + [ProducesResponseType(typeof(string), (int)HttpStatusCode.InternalServerError)] + public ObjectResult AuthenticateWithJson([FromBody] LoginDTO login) + { + return Authenticate(login.Email.ToLower(), login.Password); + } + } +} diff --git a/ManagerService/Controllers/UserController.cs b/ManagerService/Controllers/UserController.cs new file mode 100644 index 0000000..6b43e24 --- /dev/null +++ b/ManagerService/Controllers/UserController.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Manager.Interfaces.DTO; +using Manager.Interfaces.Models; +using Manager.Services; +using ManagerService.Service.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace ManagerService.Controllers +{ + [Authorize] // TODO Add ROLES (Roles = "Admin") + [Route("[controller]")] + [ApiController] + public class UserController : ControllerBase + { + private UserDatabaseService _userService; + private TokensService _tokenService; + private readonly ILogger _logger; + + public UserController(ILogger logger, UserDatabaseService userService, TokensService tokenService) + { + _logger = logger; + _userService = userService; + _tokenService = tokenService; + } + + /// + /// Get a list of user + /// + [ProducesResponseType(typeof(List), 200)] + [ProducesResponseType(typeof(string), 500)] + [HttpGet] + public ObjectResult Get() + { + try + { + List users = _userService.GetAll(); + + return new OkObjectResult(users); + } + catch (Exception ex) + { + return new ObjectResult(ex.Message) { StatusCode = 500 }; + } + } + + + /// + /// Get a specific user + /// + /// id user + [ProducesResponseType(typeof(UserDetailDTO), 200)] + [ProducesResponseType(typeof(string), 404)] + [ProducesResponseType(typeof(string), 500)] + [HttpGet("{id}")] + public ObjectResult Get(string id) + { + try + { + User user = _userService.GetById(id); + + if (user == null) + throw new KeyNotFoundException("This user was not found"); + + return new OkObjectResult(user.ToDTO()); + } + catch (KeyNotFoundException ex) + { + return new NotFoundObjectResult(ex.Message) {}; + } + catch (Exception ex) + { + return new ObjectResult(ex.Message) { StatusCode = 500 }; + } + } + + /// + /// Create an user + /// + /// New user info + [AllowAnonymous] + [ProducesResponseType(typeof(UserDetailDTO), 200)] + [ProducesResponseType(typeof(string), 400)] + [ProducesResponseType(typeof(string), 409)] + [ProducesResponseType(typeof(string), 500)] + [HttpPost] + public ObjectResult CreateUser([FromBody] User newUser) + { + try + { + if (newUser == null) + throw new ArgumentNullException("User param is null"); + + newUser.Token = _tokenService.GenerateToken(newUser.Email).ToString(); + newUser.DateCreation = DateTime.Now; + + List users = _userService.GetAll(); + + if (users.Select(u => u.Email).Contains(newUser.Email)) + throw new InvalidOperationException("This Email is already used"); + + User userCreated = _userService.Create(newUser); + + return new OkObjectResult(userCreated.ToDTO()); + } + catch (ArgumentNullException ex) + { + return new BadRequestObjectResult(ex.Message) {}; + } + catch (InvalidOperationException ex) + { + return new ConflictObjectResult(ex.Message) {}; + } + catch (Exception ex) + { + return new ObjectResult(ex.Message) { StatusCode = 500 }; + } + } + + + /// + /// Update an user + /// + /// User to update + [ProducesResponseType(typeof(UserDetailDTO), 200)] + [ProducesResponseType(typeof(string), 400)] + [ProducesResponseType(typeof(string), 404)] + [ProducesResponseType(typeof(string), 500)] + [HttpPut] + public ObjectResult UpdateUser([FromBody] User updatedUser) + { + try + { + if (updatedUser == null) + throw new ArgumentNullException("User param is null"); + + User user = _userService.GetById(updatedUser.Id); + + if (user == null) + throw new KeyNotFoundException("User does not exist"); + + User userModified = _userService.Update(updatedUser.Id, updatedUser); + + return new OkObjectResult(userModified.ToDTO()); + } + catch (ArgumentNullException ex) + { + return new BadRequestObjectResult(ex.Message) {}; + } + catch (KeyNotFoundException ex) + { + return new NotFoundObjectResult(ex.Message) {}; + } + catch (Exception ex) + { + return new ObjectResult(ex.Message) { StatusCode = 500 }; + } + } + + + /// + /// Delete an user + /// + /// Id of user to delete + [ProducesResponseType(typeof(string), 202)] + [ProducesResponseType(typeof(string), 400)] + [ProducesResponseType(typeof(string), 404)] + [ProducesResponseType(typeof(string), 500)] + [HttpDelete("{id}")] + public ObjectResult DeleteUser(string id) + { + try + { + if (id == null) + throw new ArgumentNullException("User param is null"); + + User user = _userService.GetById(id); + + if (user == null) + throw new KeyNotFoundException("User does not exist"); + + _userService.Remove(id); + + return new ObjectResult("The user has been deleted") { StatusCode = 202 }; + + } + catch (ArgumentNullException ex) + { + return new BadRequestObjectResult(ex.Message) { }; + } + catch (KeyNotFoundException ex) + { + return new NotFoundObjectResult(ex.Message) { }; + } + catch (Exception ex) + { + return new ObjectResult(ex.Message) { StatusCode = 500 }; + } + } + } +} diff --git a/ManagerService/ManagerService.csproj b/ManagerService/ManagerService.csproj new file mode 100644 index 0000000..59c045a --- /dev/null +++ b/ManagerService/ManagerService.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + diff --git a/ManagerService/Program.cs b/ManagerService/Program.cs new file mode 100644 index 0000000..cc056cb --- /dev/null +++ b/ManagerService/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ManagerService +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/ManagerService/Properties/launchSettings.json b/ManagerService/Properties/launchSettings.json new file mode 100644 index 0000000..b3e2c0e --- /dev/null +++ b/ManagerService/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:50185", + "sslPort": 44339 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_URLS": "http://*:5000/", + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "ManagerService": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/ManagerService/Security.cs b/ManagerService/Security.cs new file mode 100644 index 0000000..ba798c7 --- /dev/null +++ b/ManagerService/Security.cs @@ -0,0 +1,60 @@ +using Manager.Interfaces.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ManagerService.Service +{ + internal static class Security + { + public const string Scope = "Manager-api"; + + /// + /// Permissions + /// + private class Permissions + { + /// + /// Admin access + /// + public const string Admin = "Manager.admin"; + } + + /// + /// Custom claims types + /// + public class ClaimTypes + { + public const string Permission = "Permission"; + } + + /// + /// Permissions for each type of profile + /// + public static readonly Dictionary ProfilesConfiguration = new Dictionary() + { + // An admin has access to everything + //{ typeof(AdminProfile), new[] { Permissions.Admin} }, + }; + + /// + /// Policies names + /// + public class Policies + { + /// + /// Administration + /// + public const string Admin = "Manager.Administration"; + } + + /// + /// Policies + /// + public static readonly Policy[] PoliciesConfiguration = new[] + { + new Policy() { Name = Policies.Admin, Claims = new[] { Permissions.Admin} } + }; + } +} \ No newline at end of file diff --git a/ManagerService/Services/TokensService.cs b/ManagerService/Services/TokensService.cs new file mode 100644 index 0000000..b06fb9f --- /dev/null +++ b/ManagerService/Services/TokensService.cs @@ -0,0 +1,140 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Security.Cryptography; +using Manager.Interfaces.Models; +using Microsoft.IdentityModel.Tokens; +using Manager.Framework.Business; +using Manager.Interfaces.DTO; +using System.IdentityModel.Tokens.Jwt; + +namespace ManagerService.Service.Services +{ + /// + /// Tokens service + /// + public class TokensService + { + private readonly ILogger _logger; + private readonly TokensSettings _tokenSettings; + private readonly ProfileLogic _profileLogic; + + private readonly SigningCredentials _signingCredentials; + + /// + /// Constructor + /// + /// Logger + /// Tokens settings + /// Database context + /// Profile logic + /// Email client + public TokensService(ILogger logger, IOptions tokenSettings, ProfileLogic profileLogic) + { + _logger = logger; + _tokenSettings = tokenSettings.Value; + _profileLogic = profileLogic; + + var key = Encoding.UTF8.GetBytes(_tokenSettings.Secret); + _signingCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature); + } + + /// + /// Authenticate + /// + /// Email + /// Password + /// Token DTO in case of success + public TokenDTO Authenticate(string email, string password) + { + try + { + var claims = new List(); + var expiration = DateTime.UtcNow.AddMinutes(_tokenSettings.AccessTokenExpiration); + + // Todo nothing good here.. + var profile = _profileLogic.Authenticate(email, password); + + claims.Add(new Claim(ClaimTypes.Email, email)); + + // TODO: add refresh token support + + var tokenHandler = new JwtSecurityTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor() + { + Subject = new ClaimsIdentity(claims), + Expires = expiration, + SigningCredentials = _signingCredentials + }; + var token = tokenHandler.CreateToken(tokenDescriptor); + + return new TokenDTO() + { + access_token = tokenHandler.WriteToken(token), + expires_in = _tokenSettings.AccessTokenExpiration * 60, + expiration = new DateTimeOffset(token.ValidTo), + token_type = "Bearer", + scope = Security.Scope + }; + } + catch (UnauthorizedAccessException ex) + { + _logger?.LogError(ex, $"Authenticate error for user '{email}': unauthorized access"); + throw; + } + catch (Exception ex) + { + _logger?.LogError(ex, $"Authenticate error for user '{email}': {ex.Message}"); + throw; + } + } + + public object GenerateToken(string username) + { + var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("WVD[&vAwis9=#883bM$FRc0Mw8h")); // Put the secret in a file or something + + var claims = new Claim[] { + new Claim(ClaimTypes.Name, username), + new Claim(JwtRegisteredClaimNames.Email, "john.doe@blinkingcaret.com"), + new Claim(ClaimTypes.Role, "Admin") + }; + + var token = new JwtSecurityToken( + issuer: "Manager App", + audience: "Manager client", + claims: claims, + notBefore: DateTime.Now, + expires: DateTime.Now.AddDays(28), + signingCredentials: new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256) + ); + + string jwtToken = new JwtSecurityTokenHandler().WriteToken(token); + + return jwtToken; + } + + public static string GenerateSHA256String(string inputString) + { + SHA256 sha256 = SHA256Managed.Create(); + byte[] bytes = Encoding.UTF8.GetBytes(inputString); + byte[] hash = sha256.ComputeHash(bytes); + return GetStringFromHash(hash); + } + + public static string GetStringFromHash(byte[] hash) + { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < hash.Length; i++) + { + result.Append(hash[i].ToString("X2")); + } + return result.ToString(); + } + } +} diff --git a/ManagerService/Services/UserDatabaseService.cs b/ManagerService/Services/UserDatabaseService.cs new file mode 100644 index 0000000..5596876 --- /dev/null +++ b/ManagerService/Services/UserDatabaseService.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Manager.Interfaces.Models; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; + +namespace Manager.Services +{ + public class UserDatabaseService + { + private readonly IMongoCollection _Users; + + public UserDatabaseService(IConfiguration config) + { + var client = new MongoClient(config.GetConnectionString("TabletDb")); + var database = client.GetDatabase("TabletDb"); + _Users = database.GetCollection("Users"); + } + public List GetAll() + { + return _Users.Find(m => true).ToList(); + } + + public User GetByEmail(string email) + { + return _Users.Find(m => m.Email == email).FirstOrDefault(); + } + + public User GetById(string id) + { + return _Users.Find(m => m.Id == id).FirstOrDefault(); + } + + public bool IsExist(string id) + { + return _Users.Find(d => d.Id == id).FirstOrDefault() != null ? true : false; + } + + public User Create(User user) + { + _Users.InsertOne(user); + return user; + } + + public User Update(string id, User userIn) + { + _Users.ReplaceOne(user => user.Id == id, userIn); + return userIn; + } + + public void Remove(string id) + { + _Users.DeleteOne(user => user.Id == id); + } + + } +} diff --git a/ManagerService/Startup.cs b/ManagerService/Startup.cs new file mode 100644 index 0000000..ba7385c --- /dev/null +++ b/ManagerService/Startup.cs @@ -0,0 +1,185 @@ +using Manager.Framework.Business; +using Manager.Framework.Models; +using Manager.Interfaces.Models; +using Manager.Services; +using ManagerService.Service; +using ManagerService.Service.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using NSwag; +using NSwag.Generation.AspNetCore; +using NSwag.Generation.Processors.Security; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace ManagerService +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + // Swagger + services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); + + services.AddOpenApiDocument(config => + { + ConfigureSwagger(config); + }); + + services.AddCors(o => o.AddPolicy("AllowAll", builder => + { + builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + })); + + // Authentication + + var tokensConfiguration = Configuration.GetSection("Tokens"); + var tokenSettings = tokensConfiguration.Get(); + + services.Configure(tokensConfiguration); + + foreach (var policy in Security.PoliciesConfiguration) + services.AddAuthorization(options => + { + options.AddPolicy(policy.Name, policyAdmin => + { + foreach (var claim in policy.Claims) + policyAdmin.RequireClaim(Security.ClaimTypes.Permission, claim); + }); + }); + + services + .AddAuthentication(x => + { + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(x => + { + x.RequireHttpsMetadata = false; + x.SaveToken = true; + x.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenSettings.Secret)), + ValidateIssuer = false, + ValidateAudience = false, + RequireExpirationTime = false, + ValidateLifetime = true + }; + }); + + services.AddScoped(); + services.AddScoped(typeof(ProfileLogic)); + + services.AddScoped(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + //app.UseDeveloperExceptionPage(); + app.UseExceptionHandler(HandleError); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + app.UseOpenApi(); + app.UseSwaggerUi3(configure => + { + configure.OperationsSorter = "alpha"; + configure.TagsSorter = "alpha"; + }); + } + + private void ConfigureSwagger(AspNetCoreOpenApiDocumentGeneratorSettings config) + { + config.GenerateEnumMappingDescription = true; + config.AddSecurity("bearer", Enumerable.Empty(), new OpenApiSecurityScheme + { + Type = OpenApiSecuritySchemeType.OAuth2, + Description = "Manager Authentication", + Flow = OpenApiOAuth2Flow.Password, + Flows = new OpenApiOAuthFlows() + { + + Password = new OpenApiOAuthFlow() + { + Scopes = new Dictionary + { + { Security.Scope, "Manager WebAPI" } + }, + TokenUrl = "/api/authentication/Token", + AuthorizationUrl = "/authentication/Token", + } + } + }); + config.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("bearer")); + + config.PostProcess = document => + { + document.Info.Title = "Manager Service"; + document.Info.Description = "API description"; + document.Info.Version = "Version Pre-Alpha"; + }; + } + + + private void HandleError(IApplicationBuilder error) + { + error.Run(async context => + { + var exceptionHandlerPathFeature = context.Features.Get(); + var exception = exceptionHandlerPathFeature?.Error as RequestException; + + if (exception != null) + { + var json = exception.GetJson(); + context.Response.ContentType = "application/json"; + context.Response.StatusCode = exception.StatusCode; + await context.Response.WriteAsync(json); + } + }); + } + } +} diff --git a/ManagerService/WeatherForecast.cs b/ManagerService/WeatherForecast.cs new file mode 100644 index 0000000..93c705d --- /dev/null +++ b/ManagerService/WeatherForecast.cs @@ -0,0 +1,15 @@ +using System; + +namespace ManagerService +{ + public class WeatherForecast + { + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string Summary { get; set; } + } +} diff --git a/ManagerService/appsettings.Development.json b/ManagerService/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/ManagerService/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/ManagerService/appsettings.json b/ManagerService/appsettings.json new file mode 100644 index 0000000..3496ab4 --- /dev/null +++ b/ManagerService/appsettings.json @@ -0,0 +1,25 @@ +{ + "ConnectionStrings": { + "TabletDb": "mongodb://admin:MioTech4ever!@localhost:27017" //TO CHANGE + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "Tokens": { + "Secret": "WVD[&vAwis9=#883bM$FRc0Mw8h", + "AccessTokenExpiration": 86400, + "RefreshTokenExpiration": 518400 + }, + "SecuritySettings": { + "Secret": "kfexxgohdxeelabz", + "Issuer": "Manager", + "Audience": "the client of your app", + "IdType": "Name", + "TokenExpiryInHours": 2 + } +}