From 513f57ec72af846c34401172f8df2dd7417fe6b0 Mon Sep 17 00:00:00 2001 From: mfnefd Date: Sat, 22 Jun 2024 18:42:11 +0400 Subject: [PATCH 1/3] add two factor auth (API) --- BusinessLogic/BusinessLogic/UserLogic.cs | 17 +++++++- .../Mail/MailTemplates/MailTwoFactorCode.cs | 21 ++++++++++ BusinessLogic/Tools/TwoFactorAuthService.cs | 42 +++++++++++++++++++ .../ITwoFactorAuthService.cs | 15 +++++++ .../BusinessLogicContracts/IUserLogic.cs | 20 +++++---- RestAPI/Controllers/UserController.cs | 16 +++++++ RestAPI/Program.cs | 19 +++++---- 7 files changed, 131 insertions(+), 19 deletions(-) create mode 100644 BusinessLogic/Tools/Mail/MailTemplates/MailTwoFactorCode.cs create mode 100644 BusinessLogic/Tools/TwoFactorAuthService.cs create mode 100644 Contracts/BusinessLogicContracts/ITwoFactorAuthService.cs diff --git a/BusinessLogic/BusinessLogic/UserLogic.cs b/BusinessLogic/BusinessLogic/UserLogic.cs index ee6b806..0ccec27 100644 --- a/BusinessLogic/BusinessLogic/UserLogic.cs +++ b/BusinessLogic/BusinessLogic/UserLogic.cs @@ -25,11 +25,14 @@ namespace BusinessLogic.BusinessLogic { private readonly ILogger _logger; private readonly IUserStorage _userStorage; + private readonly ITwoFactorAuthService _twoFactorAuthService; - public UserLogic(ILogger logger, IUserStorage userStorage) + public UserLogic(ILogger logger, IUserStorage userStorage, + ITwoFactorAuthService twoFactorAuthService) { _logger = logger; _userStorage = userStorage; + _twoFactorAuthService = twoFactorAuthService; } public string Create(UserBindingModel model) @@ -53,6 +56,9 @@ namespace BusinessLogic.BusinessLogic MailSender.Send(new MailRegistration(user)); + string code = _twoFactorAuthService.GenerateCode(); + MailSender.Send(new MailTwoFactorCode(user, code)); + return JwtProvider.Generate(user); } @@ -132,6 +138,10 @@ namespace BusinessLogic.BusinessLogic { throw new AccountException("The passwords don't match."); } + + string code = _twoFactorAuthService.GenerateCode(); + MailSender.Send(new MailTwoFactorCode(user, code)); + return JwtProvider.Generate(user); } @@ -166,5 +176,10 @@ namespace BusinessLogic.BusinessLogic throw new AccountException("The email is not valid."); } } + + public bool VerifyCode(string code) + { + return _twoFactorAuthService.Verify(code); + } } } \ No newline at end of file diff --git a/BusinessLogic/Tools/Mail/MailTemplates/MailTwoFactorCode.cs b/BusinessLogic/Tools/Mail/MailTemplates/MailTwoFactorCode.cs new file mode 100644 index 0000000..6c5e4d4 --- /dev/null +++ b/BusinessLogic/Tools/Mail/MailTemplates/MailTwoFactorCode.cs @@ -0,0 +1,21 @@ +using Contracts.BindingModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BusinessLogic.Tools.Mail.MailTemplates +{ + public class MailTwoFactorCode : Mail + { + public MailTwoFactorCode(UserBindingModel user, string code) + { + To = [user.Email]; + Title = "Ваш код для подтверждения"; + Body = $"Здравствуйте, {user.SecondName} {user.FirstName}! Вот Ваш код для подтверждения:\n" + + $"{code}\n" + + $"Если это не Вы, игноритруйте это сообщение."; + } + } +} \ No newline at end of file diff --git a/BusinessLogic/Tools/TwoFactorAuthService.cs b/BusinessLogic/Tools/TwoFactorAuthService.cs new file mode 100644 index 0000000..5ab72d6 --- /dev/null +++ b/BusinessLogic/Tools/TwoFactorAuthService.cs @@ -0,0 +1,42 @@ +using Contracts.BusinessLogicContracts; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BusinessLogic.Tools +{ + public class TwoFactorAuthService : ITwoFactorAuthService + { + private string _code = string.Empty; + private const int LENGTH_CODE = 5; + + public string GenerateCode() + { + _code = _getCode(LENGTH_CODE); + return _code; + } + + private string _getCode(int length) + { + string res = ""; + var rand = new Random(); + for (int i = 0; i < length; i++) + { + res += rand.Next(0, 9).ToString(); + } + return res; + } + + public bool Verify(string code) + { + if (_code == string.Empty) + { + throw new Exception("Source code is not generated."); + } + + return _code == code; + } + } +} \ No newline at end of file diff --git a/Contracts/BusinessLogicContracts/ITwoFactorAuthService.cs b/Contracts/BusinessLogicContracts/ITwoFactorAuthService.cs new file mode 100644 index 0000000..61cea92 --- /dev/null +++ b/Contracts/BusinessLogicContracts/ITwoFactorAuthService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contracts.BusinessLogicContracts +{ + public interface ITwoFactorAuthService + { + string GenerateCode(); + + bool Verify(string code); + } +} \ No newline at end of file diff --git a/Contracts/BusinessLogicContracts/IUserLogic.cs b/Contracts/BusinessLogicContracts/IUserLogic.cs index 664b346..802f723 100644 --- a/Contracts/BusinessLogicContracts/IUserLogic.cs +++ b/Contracts/BusinessLogicContracts/IUserLogic.cs @@ -10,18 +10,20 @@ using System.Threading.Tasks; namespace Contracts.BusinessLogicContracts { - public interface IUserLogic - { - string Login(string email, string password); + public interface IUserLogic + { + string Login(string email, string password); - string Create(UserBindingModel model); + bool VerifyCode(string code); - UserViewModel Update(UserBindingModel model); + string Create(UserBindingModel model); - UserViewModel ReadElement(UserSearchModel model); + UserViewModel Update(UserBindingModel model); - IEnumerable ReadElements(UserSearchModel? model); + UserViewModel ReadElement(UserSearchModel model); - UserViewModel Delete(UserSearchModel model); - } + IEnumerable ReadElements(UserSearchModel? model); + + UserViewModel Delete(UserSearchModel model); + } } \ No newline at end of file diff --git a/RestAPI/Controllers/UserController.cs b/RestAPI/Controllers/UserController.cs index adae152..0047df3 100644 --- a/RestAPI/Controllers/UserController.cs +++ b/RestAPI/Controllers/UserController.cs @@ -46,6 +46,21 @@ namespace RestAPI.Controllers } } + [HttpPost] + public IResult VerifyCode([FromBody] VerifyCodeData data) + { + try + { + var res = _userLogic.VerifyCode(data.code); + return Results.Ok(res); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error verify code"); + return Results.Problem(ex.Message); + } + } + [HttpPost] public IResult Registration([FromBody] UserBindingModel model) { @@ -128,4 +143,5 @@ namespace RestAPI.Controllers } public record class UserData(string email, string password); + public record class VerifyCodeData(string code); } \ No newline at end of file diff --git a/RestAPI/Program.cs b/RestAPI/Program.cs index 1e6c8aa..f71b32a 100644 --- a/RestAPI/Program.cs +++ b/RestAPI/Program.cs @@ -20,6 +20,7 @@ builder.Logging.AddLog4Net("log4net.config"); builder.Services.AddTransient(); builder.Services.AddTransient(); +builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); @@ -34,7 +35,7 @@ builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { - c.SwaggerDoc(VERSION, new OpenApiInfo { Title = TITLE, Version = VERSION }); + c.SwaggerDoc(VERSION, new OpenApiInfo { Title = TITLE, Version = VERSION }); }); var app = builder.Build(); @@ -46,15 +47,15 @@ var mailSender = app.Services.GetService(); string? getSection(string section) => builder.Configuration?.GetSection(section)?.Value?.ToString(); jwtProvider?.SetupJwtOptions(new() { - SecretKey = getSection("JwtOptions:SecretKey") ?? string.Empty, - ExpiresHours = Convert.ToInt16(getSection("JwtOptions:ExpiresHours")) + SecretKey = getSection("JwtOptions:SecretKey") ?? string.Empty, + ExpiresHours = Convert.ToInt16(getSection("JwtOptions:ExpiresHours")) }); mailSender?.SetupMailOptions(new() { - Email = getSection("MailOptions:Email") ?? string.Empty, - Password = getSection("MailOptions:Password") ?? string.Empty, - SmtpClientHost = getSection("MailOptions:SmtpClientHost") ?? string.Empty, - SmtpClientPort = Convert.ToInt16(getSection("MailOptions:SmtpClientPort")) + Email = getSection("MailOptions:Email") ?? string.Empty, + Password = getSection("MailOptions:Password") ?? string.Empty, + SmtpClientHost = getSection("MailOptions:SmtpClientHost") ?? string.Empty, + SmtpClientPort = Convert.ToInt16(getSection("MailOptions:SmtpClientPort")) }); #endregion Setup config @@ -62,8 +63,8 @@ mailSender?.SetupMailOptions(new() // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(c => c.SwaggerEndpoint($"/swagger/{VERSION}/swagger.json", $"{TITLE} {VERSION}")); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint($"/swagger/{VERSION}/swagger.json", $"{TITLE} {VERSION}")); } app.UseHttpsRedirection(); -- 2.25.1 From f122886b7e8d4d6c181f82756027aabbcce69ee0 Mon Sep 17 00:00:00 2001 From: mfnefd Date: Sat, 22 Jun 2024 18:42:35 +0400 Subject: [PATCH 2/3] fix role storage --- DatabaseImplement/Implements/RoleStorage.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DatabaseImplement/Implements/RoleStorage.cs b/DatabaseImplement/Implements/RoleStorage.cs index a1d8e63..390848b 100644 --- a/DatabaseImplement/Implements/RoleStorage.cs +++ b/DatabaseImplement/Implements/RoleStorage.cs @@ -32,7 +32,7 @@ namespace DatabaseImplement.Implements public RoleBindingModel? GetElement(RoleSearchModel model) { - if (model.Id is null && string.IsNullOrWhiteSpace(model.Name)) + if (model.Id is null && string.IsNullOrWhiteSpace(model?.Name)) { return null; } @@ -46,7 +46,7 @@ namespace DatabaseImplement.Implements public IEnumerable GetList(RoleSearchModel? model) { var context = new Database(); - if (model is null && string.IsNullOrWhiteSpace(model.Name)) + if (model is null && string.IsNullOrWhiteSpace(model?.Name)) { return context.Roles.Select(r => r.GetBindingModel()); } -- 2.25.1 From 1c229ab7148ffbf318bdb91d646ee3fd27f6bd04 Mon Sep 17 00:00:00 2001 From: mfnefd Date: Sat, 22 Jun 2024 18:43:07 +0400 Subject: [PATCH 3/3] add two factor auth (Web) --- WebApp/Pages/Login.cshtml.cs | 4 +- WebApp/Pages/SignUp.cshtml.cs | 4 +- WebApp/Pages/TwoFactor.cshtml | 128 +++++++++++++++++++++++++++++++ WebApp/Pages/TwoFactor.cshtml.cs | 35 +++++++++ 4 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 WebApp/Pages/TwoFactor.cshtml create mode 100644 WebApp/Pages/TwoFactor.cshtml.cs diff --git a/WebApp/Pages/Login.cshtml.cs b/WebApp/Pages/Login.cshtml.cs index 4a53a79..46144ad 100644 --- a/WebApp/Pages/Login.cshtml.cs +++ b/WebApp/Pages/Login.cshtml.cs @@ -20,9 +20,9 @@ namespace WebApp.Pages throw new Exception("Something wrong LOL!"); } - this.SetJWT((string)response); + TempData["jwt"] = (string)response; - return RedirectToPage("Index"); + return RedirectToPage("TwoFactor"); } } } \ No newline at end of file diff --git a/WebApp/Pages/SignUp.cshtml.cs b/WebApp/Pages/SignUp.cshtml.cs index 727a532..5362104 100644 --- a/WebApp/Pages/SignUp.cshtml.cs +++ b/WebApp/Pages/SignUp.cshtml.cs @@ -30,9 +30,9 @@ namespace WebApp.Pages throw new Exception("Something wrong LOL!"); } - this.SetJWT((string)response); + TempData["jwt"] = (string)response; - return RedirectToPage("Index"); + return RedirectToPage("TwoFactor"); } } } \ No newline at end of file diff --git a/WebApp/Pages/TwoFactor.cshtml b/WebApp/Pages/TwoFactor.cshtml new file mode 100644 index 0000000..c0337fa --- /dev/null +++ b/WebApp/Pages/TwoFactor.cshtml @@ -0,0 +1,128 @@ +@page +@model WebApp.Pages.TwoFactorModel + +
+ +
+ + + +
+
+
+ ionicons-v5-g +
+

+ 2-step verification +

+

+ We sent a verification code to your email. +

+

+ Please enter the code in the field below. +

+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +

+ Didn't receive it? + + Resend code + +

+
+
\ No newline at end of file diff --git a/WebApp/Pages/TwoFactor.cshtml.cs b/WebApp/Pages/TwoFactor.cshtml.cs new file mode 100644 index 0000000..058e2d6 --- /dev/null +++ b/WebApp/Pages/TwoFactor.cshtml.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace WebApp.Pages +{ + public class TwoFactorModel : PageModel + { + [BindProperty] + public int[] Code { get; set; } = []; + + public void OnGet() + { + } + + public IActionResult OnPost(string[] code) + { + var stringCode = string.Join(string.Empty, Code); + if (string.IsNullOrEmpty(stringCode)) + { + throw new Exception("Looo"); + } + var response = (string)APIClient.PostRequest("user/verifycode", new { code = stringCode }); + var isCorrect = Convert.ToBoolean(response); + if (isCorrect) + { + this.SetJWT((string)TempData["jwt"]); + return RedirectToPage("Index"); + } + else + { + throw new Exception("Wrong code! Please retry"); + } + } + } +} \ No newline at end of file -- 2.25.1