From 4f74962ce251048855ed7d9b97b7bdfcdf10f9a3 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 21 Oct 2024 23:11:57 +0800 Subject: [PATCH] Fix #4752: validate the username and email. --- Oqtane.Client/Installer/Installer.razor | 205 +++++++++--------- .../Services/Interfaces/IUserService.cs | 9 + Oqtane.Client/Services/UserService.cs | 5 + Oqtane.Server/Controllers/UserController.cs | 7 + Oqtane.Server/Managers/InstallUserManager.cs | 41 ++++ .../Managers/Interfaces/IUserManager.cs | 1 + Oqtane.Server/Managers/UserManager.cs | 78 ++++++- Oqtane.Shared/Models/UserValidateResult.cs | 15 ++ 8 files changed, 258 insertions(+), 103 deletions(-) create mode 100644 Oqtane.Server/Managers/InstallUserManager.cs create mode 100644 Oqtane.Shared/Models/UserValidateResult.cs diff --git a/Oqtane.Client/Installer/Installer.razor b/Oqtane.Client/Installer/Installer.razor index 94f9cc39..20d4742e 100644 --- a/Oqtane.Client/Installer/Installer.razor +++ b/Oqtane.Client/Installer/Installer.razor @@ -156,129 +156,130 @@ private List _templates; private string _template = Constants.DefaultSiteTemplate; private bool _register = true; - private string _message = string.Empty; - private string _loadingDisplay = "display: none;"; + private string _message = string.Empty; + private string _loadingDisplay = "display: none;"; - protected override async Task OnInitializedAsync() - { + protected override async Task OnInitializedAsync() + { // include CSS var content = $""; SiteState.AppendHeadContent(content); _togglePassword = SharedLocalizer["ShowPassword"]; - _toggleConfirmPassword = SharedLocalizer["ShowPassword"]; + _toggleConfirmPassword = SharedLocalizer["ShowPassword"]; - _databases = await DatabaseService.GetDatabasesAsync(); - if (_databases.Exists(item => item.IsDefault)) - { - _databaseName = _databases.Find(item => item.IsDefault).Name; - } - else - { - _databaseName = "LocalDB"; - } - LoadDatabaseConfigComponent(); + _databases = await DatabaseService.GetDatabasesAsync(); + if (_databases.Exists(item => item.IsDefault)) + { + _databaseName = _databases.Find(item => item.IsDefault).Name; + } + else + { + _databaseName = "LocalDB"; + } + LoadDatabaseConfigComponent(); _templates = await SiteTemplateService.GetSiteTemplatesAsync(); } - private void DatabaseChanged(ChangeEventArgs eventArgs) - { - try - { - _databaseName = (string)eventArgs.Value; - _showConnectionString = false; - LoadDatabaseConfigComponent(); - } - catch - { - _message = Localizer["Error.DbConfig.Load"]; - } - } + private void DatabaseChanged(ChangeEventArgs eventArgs) + { + try + { + _databaseName = (string)eventArgs.Value; + _showConnectionString = false; + 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) - { + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { // include JavaScript - var interop = new Interop(JSRuntime); + var interop = new Interop(JSRuntime); await interop.IncludeScript("", Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous", "", "head"); - } - } + } + } - private async Task Install() - { - var connectionString = String.Empty; - if (_showConnectionString) - { - connectionString = _connectionString; - } - else - { - if (_databaseConfig is IDatabaseConfigControl databaseConfigControl) - { - connectionString = databaseConfigControl.GetConnectionString(); - } - } + private async Task Install() + { + var connectionString = String.Empty; + if (_showConnectionString) + { + connectionString = _connectionString; + } + else + { + if (_databaseConfig is IDatabaseConfigControl databaseConfigControl) + { + connectionString = databaseConfigControl.GetConnectionString(); + } + } - if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@")) - { - if (await UserService.ValidatePasswordAsync(_hostPassword)) - { - _loadingDisplay = ""; - StateHasChanged(); + if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@")) + { + var result = await UserService.ValidateUserAsync(_hostUsername, _hostEmail, _hostPassword); + if (result.Succeeded) + { + _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, - SiteTemplate = _template, - RenderMode = RenderModes.Static, - Runtime = Runtimes.Server - }; + 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, + SiteTemplate = _template, + RenderMode = RenderModes.Static, + Runtime = Runtimes.Server + }; - 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"]; + 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 = string.Join("
", result.Errors.Select(i => i.Value)); } } else diff --git a/Oqtane.Client/Services/Interfaces/IUserService.cs b/Oqtane.Client/Services/Interfaces/IUserService.cs index 534466a5..b32a66f0 100644 --- a/Oqtane.Client/Services/Interfaces/IUserService.cs +++ b/Oqtane.Client/Services/Interfaces/IUserService.cs @@ -113,6 +113,15 @@ namespace Oqtane.Services /// Task VerifyTwoFactorAsync(User user, string token); + /// + /// Validate identity user info. + /// + /// + /// + /// + /// + Task ValidateUserAsync(string username, string email, string password); + /// /// Validate a users password against the password policy /// diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index 2133d2de..d69aa10d 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -89,6 +89,11 @@ namespace Oqtane.Services return await PostJsonAsync($"{Apiurl}/twofactor?token={token}", user); } + public async Task ValidateUserAsync(string username, string email, string password) + { + return await GetJsonAsync($"{Apiurl}/validateuser?username={WebUtility.UrlEncode(username)}&email={WebUtility.UrlEncode(email)}&password={WebUtility.UrlEncode(password)}"); + } + 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 9e80be8d..acbcb1a1 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -347,6 +347,13 @@ namespace Oqtane.Controllers return user; } + // GET api//validate/x + [HttpGet("validateuser")] + public async Task ValidateUser(string username, string email, string password) + { + return await _userManager.ValidateUser(username, email, password); + } + // GET api//validate/x [HttpGet("validate/{password}")] public async Task Validate(string password) diff --git a/Oqtane.Server/Managers/InstallUserManager.cs b/Oqtane.Server/Managers/InstallUserManager.cs new file mode 100644 index 00000000..909b36e7 --- /dev/null +++ b/Oqtane.Server/Managers/InstallUserManager.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Oqtane.Managers +{ + /// + /// This class is only used for user validation during installation process. + /// + /// + internal class InstallUserManager : UserManager + { + public InstallUserManager(IUserStore store, IOptions optionsAccessor, IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) + { + } + + public override async Task FindByNameAsync(string userName) + { + await Task.CompletedTask; + + return null; + } + + public override async Task FindByEmailAsync(string email) + { + await Task.CompletedTask; + + return null; + } + + public override async Task GetUserIdAsync(IdentityUser user) + { + await Task.CompletedTask; + + return null; + } + } +} diff --git a/Oqtane.Server/Managers/Interfaces/IUserManager.cs b/Oqtane.Server/Managers/Interfaces/IUserManager.cs index 5ada9827..4fde062c 100644 --- a/Oqtane.Server/Managers/Interfaces/IUserManager.cs +++ b/Oqtane.Server/Managers/Interfaces/IUserManager.cs @@ -19,6 +19,7 @@ namespace Oqtane.Managers Task ResetPassword(User user, string token); User VerifyTwoFactor(User user, string token); Task LinkExternalAccount(User user, string token, string type, string key, string name); + Task ValidateUser(string username, string email, string password); Task ValidatePassword(string password); Task> ImportUsers(int siteId, string filePath, bool notify); } diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index e0e92e97..76e0c05d 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -33,8 +33,41 @@ namespace Oqtane.Managers private readonly ILogManager _logger; private readonly IMemoryCache _cache; private readonly IStringLocalizer _localizer; + private readonly IUserStore _identityStore; + private readonly Microsoft.Extensions.Options.IOptions _identityOptionsAccessor; + private readonly IPasswordHasher _passwordHasher; + private readonly IEnumerable> _userValidators; + private readonly IEnumerable> _passwordValidators; + private readonly ILookupNormalizer _identityKeyNormalizer; + private readonly IdentityErrorDescriber _identityErrors; + private readonly IServiceProvider _identityServices; + private readonly Microsoft.Extensions.Logging.ILogger> _identityLogger; - public UserManager(IUserRepository users, IRoleRepository roles, IUserRoleRepository userRoles, UserManager identityUserManager, SignInManager identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, IProfileRepository profiles, ISettingRepository settings, ISiteRepository sites, ISyncManager syncManager, ILogManager logger, IMemoryCache cache, IStringLocalizer localizer) + public UserManager( + IUserRepository users, + IRoleRepository roles, + IUserRoleRepository userRoles, + UserManager identityUserManager, + SignInManager identitySignInManager, + ITenantManager tenantManager, + INotificationRepository notifications, + IFolderRepository folders, + IProfileRepository profiles, + ISettingRepository settings, + ISiteRepository sites, + ISyncManager syncManager, + ILogManager logger, + IMemoryCache cache, + IStringLocalizer localizer, + IUserStore store, + Microsoft.Extensions.Options.IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, + Microsoft.Extensions.Logging.ILogger> identityLogger) { _users = users; _roles = roles; @@ -51,6 +84,15 @@ namespace Oqtane.Managers _logger = logger; _cache = cache; _localizer = localizer; + _identityStore = store; + _identityOptionsAccessor = optionsAccessor; + _passwordHasher = passwordHasher; + _userValidators = userValidators; + _passwordValidators = passwordValidators; + _identityKeyNormalizer = keyNormalizer; + _identityErrors = errors; + _identityServices = services; + _identityLogger = identityLogger; } public User GetUser(int userid, int siteid) @@ -540,6 +582,40 @@ namespace Oqtane.Managers return user; } + public async Task ValidateUser(string username, string email, string password) + { + var validateResult = new UserValidateResult { Succeeded = true }; + var installUserManager = new InstallUserManager(_identityStore, _identityOptionsAccessor, _passwordHasher, _userValidators, _passwordValidators, _identityKeyNormalizer, _identityErrors, _identityServices, _identityLogger); + + var user = new IdentityUser { UserName = username, Email = email, EmailConfirmed = true }; + var userValidator = new UserValidator(); + var userResult = await userValidator.ValidateAsync(installUserManager, user); + if (!userResult.Succeeded) + { + validateResult.Succeeded = false; + if(userResult.Errors != null) + { + validateResult.Errors = userResult.Errors?.ToDictionary(i => i.Code, i => i.Description); + } + } + + var passwordValidator = new PasswordValidator(); + var passwordResult = await passwordValidator.ValidateAsync(installUserManager, null, password); + if (!passwordResult.Succeeded && !validateResult.Errors.ContainsKey("InvalidPassword")) + { + validateResult.Succeeded = false; + if (passwordResult.Errors != null) + { + foreach (var error in passwordResult.Errors) + { + validateResult.Errors.Add(error.Code, error.Description); + } + } + } + + return validateResult; + } + public async Task ValidatePassword(string password) { var validator = new PasswordValidator(); diff --git a/Oqtane.Shared/Models/UserValidateResult.cs b/Oqtane.Shared/Models/UserValidateResult.cs new file mode 100644 index 00000000..d531ab82 --- /dev/null +++ b/Oqtane.Shared/Models/UserValidateResult.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Oqtane.Models +{ + public class UserValidateResult + { + public bool Succeeded { get; set; } + + public IDictionary Errors { get; set; } = new Dictionary(); + } +}