From f250aff99b43666cba0086eaa22433433e8c6276 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Tue, 8 Mar 2022 08:31:18 -0500 Subject: [PATCH] Added password policy validation in install wizard --- Oqtane.Client/Installer/Installer.razor | 155 +++++++++--------- .../Resources/Installer/Installer.resx | 9 +- .../Services/Interfaces/IUserService.cs | 8 + Oqtane.Client/Services/UserService.cs | 6 + Oqtane.Server/Controllers/UserController.cs | 31 ++-- 5 files changed, 121 insertions(+), 88 deletions(-) diff --git a/Oqtane.Client/Installer/Installer.razor b/Oqtane.Client/Installer/Installer.razor index 75343a49..2dca6de9 100644 --- a/Oqtane.Client/Installer/Installer.razor +++ b/Oqtane.Client/Installer/Installer.razor @@ -121,90 +121,97 @@ { _databaseName = "LocalDB"; } - LoadDatabaseConfigComponent(); - } + LoadDatabaseConfigComponent(); + } - private void DatabaseChanged(ChangeEventArgs eventArgs) - { - try - { - _databaseName = (string)eventArgs.Value; + private void DatabaseChanged(ChangeEventArgs eventArgs) + { + try + { + _databaseName = (string)eventArgs.Value; - LoadDatabaseConfigComponent(); - } - catch - { - _message = Localizer["Error.DbConfig.Load"]; - } - } + LoadDatabaseConfigComponent(); + } + catch + { + _message = Localizer["Error.DbConfig.Load"]; + } + } - private void LoadDatabaseConfigComponent() - { - var database = _databases.SingleOrDefault(d => d.Name == _databaseName); - if (database != null) - { - _databaseConfigType = Type.GetType(database.ControlType); - DatabaseConfigComponent = builder => - { - builder.OpenComponent(0, _databaseConfigType); - builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); }); - builder.CloseComponent(); - }; - } - } + private void LoadDatabaseConfigComponent() + { + var database = _databases.SingleOrDefault(d => d.Name == _databaseName); + if (database != null) + { + _databaseConfigType = Type.GetType(database.ControlType); + DatabaseConfigComponent = builder => + { + builder.OpenComponent(0, _databaseConfigType); + builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); }); + builder.CloseComponent(); + }; + } + } - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - var interop = new Interop(JSRuntime); - await interop.IncludeLink("", "stylesheet", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css", "text/css", "sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w==", "anonymous", ""); - await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js", "sha512-pax4MlgXjHEPfCwcJLQhigY7+N8rt6bVvWLFyUMuxShv170X53TRzGPmPkZmGBhk+jikR8WBM4yl7A9WMHHqvg==", "anonymous", "", "head", ""); - } - } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + var interop = new Interop(JSRuntime); + await interop.IncludeLink("", "stylesheet", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css", "text/css", "sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w==", "anonymous", ""); + await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js", "sha512-pax4MlgXjHEPfCwcJLQhigY7+N8rt6bVvWLFyUMuxShv170X53TRzGPmPkZmGBhk+jikR8WBM4yl7A9WMHHqvg==", "anonymous", "", "head", ""); + } + } - private async Task Install() - { - var connectionString = String.Empty; - if (_databaseConfig is IDatabaseConfigControl databaseConfigControl) - { - connectionString = databaseConfigControl.GetConnectionString(); - } + private async Task Install() + { + var connectionString = String.Empty; + if (_databaseConfig is IDatabaseConfigControl databaseConfigControl) + { + connectionString = databaseConfigControl.GetConnectionString(); + } - if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && _hostPassword.Length >= 6 && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@")) - { - _loadingDisplay = ""; - StateHasChanged(); + if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@")) + { + if (await UserService.ValidatePasswordAsync(_hostPassword)) + { + _loadingDisplay = ""; + StateHasChanged(); - Uri uri = new Uri(NavigationManager.Uri); + Uri uri = new Uri(NavigationManager.Uri); - var database = _databases.SingleOrDefault(d => d.Name == _databaseName); + var database = _databases.SingleOrDefault(d => d.Name == _databaseName); - var config = new InstallConfig - { - DatabaseType = database.DBType, - ConnectionString = connectionString, - Aliases = uri.Authority, - HostUsername = _hostUsername, - HostPassword = _hostPassword, - HostEmail = _hostEmail, - HostName = _hostUsername, - TenantName = TenantNames.Master, - IsNewTenant = true, - SiteName = Constants.DefaultSite, - Register = _register - }; + var config = new InstallConfig + { + DatabaseType = database.DBType, + ConnectionString = connectionString, + Aliases = uri.Authority, + HostUsername = _hostUsername, + HostPassword = _hostPassword, + HostEmail = _hostEmail, + HostName = _hostUsername, + TenantName = TenantNames.Master, + IsNewTenant = true, + SiteName = Constants.DefaultSite, + Register = _register + }; - var installation = await InstallationService.Install(config); - if (installation.Success) - { - NavigationManager.NavigateTo(uri.Scheme + "://" + uri.Authority, true); - } - else - { - _message = installation.Message; - _loadingDisplay = "display: none;"; - } + var installation = await InstallationService.Install(config); + if (installation.Success) + { + NavigationManager.NavigateTo(uri.Scheme + "://" + uri.Authority, true); + } + else + { + _message = installation.Message; + _loadingDisplay = "display: none;"; + } + } + else + { + _message = Localizer["Message.Password.Invalid"]; + } } else { diff --git a/Oqtane.Client/Resources/Installer/Installer.resx b/Oqtane.Client/Resources/Installer/Installer.resx index ecbce69e..740ba227 100644 --- a/Oqtane.Client/Resources/Installer/Installer.resx +++ b/Oqtane.Client/Resources/Installer/Installer.resx @@ -130,12 +130,15 @@ Install Now - Error loading Database Configuration Control + Error Loading Database Configuration Control - Please Enter All Required Fields. Ensure Passwords Match And Are Greater Than 5 Characters In Length. Ensure Email Address Provided Is Valid. + Please Enter All Required Fields. Ensure Passwords Match And Email Address Provided Is Valid. - + + The Password Provided Does Not Meet The Password Policy. Please Verify The Minimum Password Length And Complexity Requirements. + + Please Register Me For Major Product Updates And Security Bulletins diff --git a/Oqtane.Client/Services/Interfaces/IUserService.cs b/Oqtane.Client/Services/Interfaces/IUserService.cs index 28258449..483e5284 100644 --- a/Oqtane.Client/Services/Interfaces/IUserService.cs +++ b/Oqtane.Client/Services/Interfaces/IUserService.cs @@ -96,5 +96,13 @@ namespace Oqtane.Services /// /// Task VerifyTwoFactorAsync(User user, string token); + + /// + /// Validate a users password against the password policy + /// + /// + /// + Task ValidatePasswordAsync(string password); + } } diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index 77a83005..f8eeeef7 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -3,6 +3,7 @@ using Oqtane.Models; using System.Net.Http; using System.Threading.Tasks; using Oqtane.Documentation; +using System.Net; namespace Oqtane.Services { @@ -73,5 +74,10 @@ namespace Oqtane.Services { return await PostJsonAsync($"{Apiurl}/twofactor?token={token}", user); } + + public async Task ValidatePasswordAsync(string password) + { + return await GetJsonAsync($"{Apiurl}/validate/{WebUtility.UrlEncode(password)}"); + } } } diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 9128c8aa..4f938c69 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -26,12 +26,12 @@ namespace Oqtane.Controllers private readonly IUserRoleRepository _userRoles; private readonly UserManager _identityUserManager; private readonly SignInManager _identitySignInManager; + private readonly ITenantManager _tenantManager; private readonly INotificationRepository _notifications; private readonly IFolderRepository _folders; private readonly ISyncManager _syncManager; private readonly ISiteRepository _sites; private readonly ILogManager _logger; - private readonly Alias _alias; public UserController(IUserRepository users, IRoleRepository roles, IUserRoleRepository userRoles, UserManager identityUserManager, SignInManager identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, ISyncManager syncManager, ISiteRepository sites, ILogManager logger) { @@ -40,12 +40,12 @@ namespace Oqtane.Controllers _userRoles = userRoles; _identityUserManager = identityUserManager; _identitySignInManager = identitySignInManager; + _tenantManager = tenantManager; _folders = folders; _notifications = notifications; _syncManager = syncManager; _sites = sites; _logger = logger; - _alias = tenantManager.GetAlias(); } // GET api//5?siteid=x @@ -54,7 +54,7 @@ namespace Oqtane.Controllers public User Get(int id, string siteid) { int SiteId; - if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) + if (int.TryParse(siteid, out SiteId) && SiteId == _tenantManager.GetAlias().SiteId) { User user = _users.GetUser(id); if (user != null) @@ -77,7 +77,7 @@ namespace Oqtane.Controllers public User Get(string name, string siteid) { int SiteId; - if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) + if (int.TryParse(siteid, out SiteId) && SiteId == _tenantManager.GetAlias().SiteId) { User user = _users.GetUser(name); if (user != null) @@ -129,7 +129,7 @@ namespace Oqtane.Controllers [HttpPost] public async Task Post([FromBody] User user) { - if (ModelState.IsValid && user.SiteId == _alias.SiteId) + if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId) { var User = await CreateUser(user); return User; @@ -178,7 +178,7 @@ namespace Oqtane.Controllers if (!verified) { string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); - string url = HttpContext.Request.Scheme + "://" + _alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); + string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); string body = "Dear " + user.DisplayName + ",\n\nIn Order To Complete The Registration Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!"; var notification = new Notification(user.SiteId, newUser, "User Account Verification", body); _notifications.AddNotification(notification); @@ -252,7 +252,7 @@ namespace Oqtane.Controllers [Authorize] public async Task Put(int id, [FromBody] User user) { - if (ModelState.IsValid && user.SiteId == _alias.SiteId && _users.GetUser(user.UserId, false) != null && (User.IsInRole(RoleNames.Admin) || User.Identity.Name == user.Username)) + if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && _users.GetUser(user.UserId, false) != null && (User.IsInRole(RoleNames.Admin) || User.Identity.Name == user.Username)) { if (user.Password != "") { @@ -264,7 +264,7 @@ namespace Oqtane.Controllers } } user = _users.UpdateUser(user); - _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.User, user.UserId); + _syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId); user.Password = ""; // remove sensitive information _logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user); } @@ -285,7 +285,7 @@ namespace Oqtane.Controllers { int SiteId; User user = _users.GetUser(id); - if (user != null && int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) + if (user != null && int.TryParse(siteid, out SiteId) && SiteId == _tenantManager.GetAlias().SiteId) { // remove user roles for site foreach (UserRole userrole in _userRoles.GetUserRoles(user.UserId, SiteId).ToList()) @@ -396,7 +396,7 @@ namespace Oqtane.Controllers { user = _users.GetUser(user.Username); string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser); - string url = HttpContext.Request.Scheme + "://" + _alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); + string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); string body = "Dear " + user.DisplayName + ",\n\nYou attempted multiple times unsuccessfully to log in to your account and it is now locked out. Please wait a few minutes and then try again... or use the link below to reset your password:\n\n" + url + "\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." + "\n\nThank You!"; @@ -464,7 +464,7 @@ namespace Oqtane.Controllers { user = _users.GetUser(user.Username); string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser); - string url = HttpContext.Request.Scheme + "://" + _alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); + string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); string body = "Dear " + user.DisplayName + ",\n\nYou recently requested to reset your password. Please use the link below to complete the process:\n\n" + url + "\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." + "\n\nIf you did not request to reset your password you can safely ignore this message." + @@ -532,6 +532,15 @@ namespace Oqtane.Controllers return loginUser; } + // GET api//validate/x + [HttpGet("validate/{password}")] + public async Task Validate(string password) + { + var validator = new PasswordValidator(); + var result = await validator.ValidateAsync(_identityUserManager, null, password); + return result.Succeeded; + } + // GET api//authenticate [HttpGet("authenticate")] public User Authenticate()