diff --git a/src/WorkFlowCheck.API/Controllers/UserController.cs b/src/WorkFlowCheck.API/Controllers/UserController.cs index 3bf2eab..b7bb68e 100644 --- a/src/WorkFlowCheck.API/Controllers/UserController.cs +++ b/src/WorkFlowCheck.API/Controllers/UserController.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using WorkFlowCheck.Common.DTO; using WorkFlowCheck.BL.Services.Interfaces; @@ -114,6 +114,7 @@ namespace WorkFlowCheck.API.Controllers } return retVal; } + [HttpPost("Authenticate")] public async Task> Authenticate([FromBody] UserSimpleDTO userSimpleDTO) { @@ -124,7 +125,7 @@ namespace WorkFlowCheck.API.Controllers if (string.IsNullOrEmpty(userSimpleDTO.UserName) || string.IsNullOrEmpty(userSimpleDTO.Password)) { retVal.IsSuccess = false; - retVal.Errors.Add("Nem megfelelo felhasználói név vagy jelszó"); + retVal.Errors.Add("Nem megfelelo felhasználói név vagy jelszó"); return retVal; } try @@ -136,11 +137,11 @@ namespace WorkFlowCheck.API.Controllers { retVal.IsSuccess = true; retVal.Data = userDTO_Response; - Log.Information($"Sikeres azonosítás! {userSimpleDTO.UserName}"); + Log.Information($"Sikeres azonosítás! {userSimpleDTO.UserName}"); } else { - var error = $"Nem megfelelo felhasználó vagy jelszó! {userSimpleDTO.UserName}"; + var error = $"Nem megfelelo felhasználó vagy jelszó! {userSimpleDTO.UserName}"; retVal.IsSuccess = false; retVal.Errors.Add(error); Log.Information(error); @@ -148,7 +149,49 @@ namespace WorkFlowCheck.API.Controllers } else { - var error = $"Nem megfelelo felhasználó vagy jelszó! {userSimpleDTO.UserName}"; + var error = $"Nem megfelelo felhasználó vagy jelszó! {userSimpleDTO.UserName}"; + retVal.IsSuccess = false; + retVal.Errors.Add(error); + Log.Information(error); + } + } + catch (Exception ex) + { + + Log.Error(ex.Message); + } + return retVal; + } + /// + /// Ez a 2 faktoros elsÅ‘ lépése. + /// + /// + /// + [HttpPost("Authenticate2F1")] + public async Task> Authenticate2F1([FromBody] UserSimpleDTO userSimpleDTO) + { + var retVal = new ApiResponseDTO() + { + IsSuccess = true + }; + if (string.IsNullOrEmpty(userSimpleDTO.UserName) || string.IsNullOrEmpty(userSimpleDTO.Password)) + { + retVal.IsSuccess = false; + retVal.Errors.Add("Nem megfelelo felhasználói név vagy jelszó"); + return retVal; + } + try + { + var response = await _userService.Authenticate2F1(userSimpleDTO.UserName, userSimpleDTO.Password); + if (response != null) + { + retVal.IsSuccess = true; + retVal.Data = response; + Log.Information($"Sikeres azonosítás! {userSimpleDTO.UserName}"); + } + else + { + var error = $"Nem megfelelo felhasználó vagy jelszó! {userSimpleDTO.UserName}"; retVal.IsSuccess = false; retVal.Errors.Add(error); Log.Information(error); @@ -171,7 +214,7 @@ namespace WorkFlowCheck.API.Controllers if (string.IsNullOrEmpty(userSimpleDTO.UserName) || string.IsNullOrEmpty(userSimpleDTO.Password)) { retVal.IsSuccess = false; - retVal.Errors.Add("Nem megfelelo felhasználói név vagy jelszó"); + retVal.Errors.Add("Nem megfelelo felhasználói név vagy jelszó"); return retVal; } try @@ -183,11 +226,11 @@ namespace WorkFlowCheck.API.Controllers { retVal.IsSuccess = true; retVal.Data = userDTO_Response; - Log.Information($"Sikeres azonosítás! {userSimpleDTO.UserName}"); + Log.Information($"Sikeres azonosítás! {userSimpleDTO.UserName}"); } else { - var error = $"Nem megfelelo felhasználó vagy jelszó! {userSimpleDTO.UserName}"; + var error = $"Nem megfelelo felhasználó vagy jelszó! {userSimpleDTO.UserName}"; retVal.IsSuccess = false; retVal.Errors.Add(error); Log.Information(error); @@ -195,7 +238,7 @@ namespace WorkFlowCheck.API.Controllers } else { - var error = $"Nem megfelelo felhasználó vagy jelszó! {userSimpleDTO.UserName}"; + var error = $"Nem megfelelo felhasználó vagy jelszó! {userSimpleDTO.UserName}"; retVal.IsSuccess = false; retVal.Errors.Add(error); Log.Information(error); diff --git a/src/WorkFlowCheck.API/appsettings.json b/src/WorkFlowCheck.API/appsettings.json index 69b2754..79a86ab 100644 --- a/src/WorkFlowCheck.API/appsettings.json +++ b/src/WorkFlowCheck.API/appsettings.json @@ -7,7 +7,7 @@ "SecretKey": "9aLxV2t7RpM8BfKqZwXy3GehYJDUNTCrHkVZ", "Issuer": "https://wfcapi.nuvolar.hu/", "Audience": "wfc-application", - "TokenLifetimeMinutes": 30 + "TokenLifetimeMinutes": 720 }, "Logging": { "LogLevel": { diff --git a/src/WorkFlowCheck.BL/HtmlTemplates/Token2FBody.html b/src/WorkFlowCheck.BL/HtmlTemplates/Token2FBody.html new file mode 100644 index 0000000..a9c2abe --- /dev/null +++ b/src/WorkFlowCheck.BL/HtmlTemplates/Token2FBody.html @@ -0,0 +1,18 @@ + + + + + MegerÅ‘sítÅ‘ kód + + + +

+ Workflow Check Application Web megerősítő kódja: +

+ +

+ #token2F# +

+ + + \ No newline at end of file diff --git a/src/WorkFlowCheck.BL/Services/Interfaces/IUserService.cs b/src/WorkFlowCheck.BL/Services/Interfaces/IUserService.cs index 45e48c9..1c76d95 100644 --- a/src/WorkFlowCheck.BL/Services/Interfaces/IUserService.cs +++ b/src/WorkFlowCheck.BL/Services/Interfaces/IUserService.cs @@ -14,6 +14,8 @@ namespace WorkFlowCheck.BL.Services.Interfaces Task DeleteUserAsync(int Id); Task UpdateUserAsync(UserDTO userDTO); Task Authenticate(string UserName, string Password); + Task Authenticate2F1(string UserName, string Password); + Task Authenticate2F2(string UserName, string Password, string token2FA); Task AuthenticateNFC(string NFCCode); diff --git a/src/WorkFlowCheck.BL/Services/UserService.cs b/src/WorkFlowCheck.BL/Services/UserService.cs index bef3d15..a017fda 100644 --- a/src/WorkFlowCheck.BL/Services/UserService.cs +++ b/src/WorkFlowCheck.BL/Services/UserService.cs @@ -16,17 +16,24 @@ using System.Security.Claims; using WorkFlowCheck.DL.Entities; using Microsoft.Extensions.Configuration; using Microsoft.AspNetCore.Identity; +using WorkFlowCheck.BL.Static; +using System.Reflection; namespace WorkFlowCheck.BL.Services { public class UserService : BaseService, IUserService { private IConfiguration _configuration; + private IMessageService _messageService; - public UserService(AppDbContext dbContext, IMapper mapper, IConfiguration configuration) : base(dbContext, mapper) + public UserService(AppDbContext dbContext, + IMapper mapper, + IConfiguration configuration, + IMessageService messageService) : base(dbContext, mapper) { _configuration = configuration; + _messageService = messageService; } public async Task GetUserAsync(int Id) @@ -90,7 +97,6 @@ namespace WorkFlowCheck.BL.Services .FirstOrDefaultAsync(); if (user != null && PasswordHasher.VerifyPassword(user.PasswordHash, password)) { - var userDTO = new UserDTO() { Id = user.Id, @@ -117,6 +123,46 @@ namespace WorkFlowCheck.BL.Services } return retVal; } + public async Task Authenticate2F1(string userName, string password) + { + var retVal = ""; + try + { + var user = await _dbContext.Users + .Include(i => i.UserRoles) + .ThenInclude(i => i.Role) + .Where(w => w.UserName == userName) + .FirstOrDefaultAsync(); + if (user != null && PasswordHasher.VerifyPassword(user.PasswordHash, password)) + { + if (!string.IsNullOrEmpty(user.Email)) + { + var token2F = CodeGenerator2F.GenerateSixDigitCode(); + + await _messageService.SendMailAsync(new List() + { + user.Email, + }, "Bejelentkezés megerősítése", + Get2FConfirmationMessageBody(token2F)); + + retVal = CodeGenerator2F.GenerateToken2F(userName, password, token2F); + return retVal; + } + } + } + catch (Exception ex) + { + Log.Error(ex.Message); + } + return retVal; + } + public async Task Authenticate2F2(string UserName, string Password, string token2FA) + { + var retVal = new UserDTO() { RoleDTO = new List() }; + + + return retVal; + } public async Task AuthenticateNFC(string NFCCode) { var retVal = new UserDTO() { RoleDTO = new List() }; @@ -241,6 +287,20 @@ namespace WorkFlowCheck.BL.Services return retVal; } + private string Get2FConfirmationMessageBody(string token2F) + { + var assembly = Assembly.GetExecutingAssembly(); + var resourceName = "WorkFlowCheck.BL.HtmlTemplates.Token2FBody.html"; + + using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream == null) + throw new FileNotFoundException($"Nem található a beágyazott erőforrás: {resourceName}"); + + using var reader = new StreamReader(stream); + var html = reader.ReadToEnd(); + return html.Replace("#token2F#", token2F); + } + public async Task GetRoleAsync(int Id) { var retVal = new RoleDTO(); diff --git a/src/WorkFlowCheck.BL/Static/CodeGenerator2F.cs b/src/WorkFlowCheck.BL/Static/CodeGenerator2F.cs new file mode 100644 index 0000000..5da052b --- /dev/null +++ b/src/WorkFlowCheck.BL/Static/CodeGenerator2F.cs @@ -0,0 +1,72 @@ +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; + +namespace WorkFlowCheck.BL.Static +{ + public static class CodeGenerator2F + { + private static readonly Random _random = new Random(); + private const string SecretKey = "874512WcfApplicationNuvolar.hu!%mBX210jUUKL"; + private static readonly byte[] KeyBytes = Encoding.UTF8.GetBytes(SecretKey); + + public static string GenerateSixDigitCode() + { + return _random.Next(100000, 1000000).ToString(); // 100000 - 999999 + } + public static string GenerateToken2F(string value1, string value2, string value3) + { + var tokenHandler = new JwtSecurityTokenHandler(); + + var claims = new[] + { + new Claim("val1", value1), + new Claim("val2", value2), + new Claim("val3", value3) + }; + + var descriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims), + Expires = DateTime.UtcNow.AddSeconds(60), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(KeyBytes), SecurityAlgorithms.HmacSha256Signature) + }; + + var token = tokenHandler.CreateToken(descriptor); + return tokenHandler.WriteToken(token); + } + public static bool ValidateToken2F(string token, string expected1, string expected2, string expected3) + { + var tokenHandler = new JwtSecurityTokenHandler(); + + try + { + var parameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(KeyBytes), + ClockSkew = TimeSpan.Zero // nincs türelmi idő + }; + + var principal = tokenHandler.ValidateToken(token, parameters, out var validatedToken); + + var val1 = principal.FindFirst("val1")?.Value; + var val2 = principal.FindFirst("val2")?.Value; + var val3 = principal.FindFirst("val3")?.Value; + + return val1 == expected1 && val2 == expected2 && val3 == expected3; + } + catch + { + return false; + } + } + } +} diff --git a/src/WorkFlowCheck.BL/WorkFlowCheck.BL.csproj b/src/WorkFlowCheck.BL/WorkFlowCheck.BL.csproj index be4021a..eb5ed4f 100644 --- a/src/WorkFlowCheck.BL/WorkFlowCheck.BL.csproj +++ b/src/WorkFlowCheck.BL/WorkFlowCheck.BL.csproj @@ -6,6 +6,14 @@ enable + + + + + + + + diff --git a/src/WorkFlowCheck.Common/DTO/UserDTO.cs b/src/WorkFlowCheck.Common/DTO/UserDTO.cs index f43bb95..705245b 100644 --- a/src/WorkFlowCheck.Common/DTO/UserDTO.cs +++ b/src/WorkFlowCheck.Common/DTO/UserDTO.cs @@ -8,6 +8,8 @@ namespace WorkFlowCheck.Common.DTO public int Id { get; set; } [Required(ErrorMessage = "Az utónév kötelező.")] + [EmailAddress(ErrorMessage = "Érvénytelen email cím.")] + [RegularExpression(@"^[^@\s]+@[^@\s]+\.[^@\s]+$", ErrorMessage = "Az email formátum nem megfelelő.")] [DisplayName("E-mail")] public string Email { get; set; } = null!; diff --git a/src/WorkFlowCheck.Web/Pages/UserAndRole/UserEditPage.cshtml b/src/WorkFlowCheck.Web/Pages/UserAndRole/UserEditPage.cshtml index c13a8a5..0ed640e 100644 --- a/src/WorkFlowCheck.Web/Pages/UserAndRole/UserEditPage.cshtml +++ b/src/WorkFlowCheck.Web/Pages/UserAndRole/UserEditPage.cshtml @@ -1,5 +1,7 @@ @page @model WorkFlowCheck.Web.Pages.UserAndRole.UserEditPageModel +@using WorkFlowCheck.Common.Helper +@using WorkFlowCheck.Common.DTO @using Microsoft.AspNetCore.Antiforgery @inject IAntiforgery Antiforgery @{ @@ -82,13 +84,25 @@ ID - Role Name + @DisplayNameHelper.GetDisplayName(nameof(RoleDTO.RoleName), typeof(RoleDTO)) + @DisplayNameHelper.GetDisplayName(nameof(RoleDTO.ParentRoleName), typeof(RoleDTO)) + @DisplayNameHelper.GetDisplayName(nameof(RoleDTO.IsAdmin), typeof(RoleDTO)) + @DisplayNameHelper.GetDisplayName(nameof(RoleDTO.CanEnableBlocked), typeof(RoleDTO)) + @DisplayNameHelper.GetDisplayName(nameof(RoleDTO.CanDownloadAPK), typeof(RoleDTO)) + @DisplayNameHelper.GetDisplayName(nameof(RoleDTO.CanUseMobilApp), typeof(RoleDTO)) + @DisplayNameHelper.GetDisplayName(nameof(RoleDTO.CanUseWebAdmin), typeof(RoleDTO)) ID - Role Name + @DisplayNameHelper.GetDisplayName(nameof(RoleDTO.RoleName), typeof(RoleDTO)) + @DisplayNameHelper.GetDisplayName(nameof(RoleDTO.ParentRoleName), typeof(RoleDTO)) + @DisplayNameHelper.GetDisplayName(nameof(RoleDTO.IsAdmin), typeof(RoleDTO)) + @DisplayNameHelper.GetDisplayName(nameof(RoleDTO.CanEnableBlocked), typeof(RoleDTO)) + @DisplayNameHelper.GetDisplayName(nameof(RoleDTO.CanDownloadAPK), typeof(RoleDTO)) + @DisplayNameHelper.GetDisplayName(nameof(RoleDTO.CanUseMobilApp), typeof(RoleDTO)) + @DisplayNameHelper.GetDisplayName(nameof(RoleDTO.CanUseWebAdmin), typeof(RoleDTO)) @@ -106,7 +120,57 @@ }, columns: [ { data: "id" }, - { data: "roleName" } + { data: "roleName" }, + { data: "parentRoleName" }, + { + data: "isAdmin", + searchable: false, + sortable: false, + className: "text-center", + render: function ( data, type, row ) { + return renderCheckBox(data); + } + + }, + { + data: "canEnableBlocked", + searchable: false, + sortable: false, + className: "text-center", + render: function ( data, type, row ) { + return renderCheckBox(data); + } + + }, + { + data: "canDownloadAPK", + searchable: false, + sortable: false, + className: "text-center", + render: function ( data, type, row ) { + return renderCheckBox(data); + } + + }, + { + data: "canUseMobilApp", + searchable: false, + sortable: false, + className: "text-center", + render: function ( data, type, row ) { + return renderCheckBox(data); + } + + }, + { + data: "canUseWebAdmin", + searchable: false, + sortable: false, + className: "text-center", + render: function ( data, type, row ) { + return renderCheckBox(data); + } + }, ], columnDefs: [ {