diff --git a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs index 9570676ebaa2..ac0727889dc4 100644 --- a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs @@ -139,6 +139,7 @@ public class SecuritySettings /// The user login endpoint ensures that failed login attempts take at least as long as the average successful login. /// However, if no successful logins have occurred, this value is used as the default duration. /// + [Range(0, int.MaxValue)] // TODO (V17): Change property type to short and update maximum range to short.MaxValue [DefaultValue(StaticUserDefaultFailedLoginDurationInMilliseconds)] public long UserDefaultFailedLoginDurationInMilliseconds { get; set; } = StaticUserDefaultFailedLoginDurationInMilliseconds; @@ -148,6 +149,7 @@ public class SecuritySettings /// /// The minimum duration (in milliseconds) of failed login attempts. /// + [Range(0, int.MaxValue)] // TODO (V17): Change property type to short and update maximum range to short.MaxValue [DefaultValue(StaticUserMinimumFailedLoginDurationInMilliseconds)] public long UserMinimumFailedLoginDurationInMilliseconds { get; set; } = StaticUserMinimumFailedLoginDurationInMilliseconds; } diff --git a/src/Umbraco.Core/TimedScope.cs b/src/Umbraco.Core/TimedScope.cs index f12f0e90eda8..6cd009f3035c 100644 --- a/src/Umbraco.Core/TimedScope.cs +++ b/src/Umbraco.Core/TimedScope.cs @@ -133,6 +133,8 @@ public void Dispose() { Thread.Sleep(remaining); } + + _cancellationTokenSource.Dispose(); } /// @@ -151,6 +153,8 @@ public async ValueTask DisposeAsync() { await Task.Delay(remaining, _timeProvider, _cancellationTokenSource.Token).ConfigureAwait(false); } + + _cancellationTokenSource.Dispose(); } private bool TryGetRemaining(out TimeSpan remaining) diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 484a80ac7ef0..ca9cfedf3615 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -74,7 +74,6 @@ public class AuthenticationController : UmbracoApiControllerBase private readonly IUserService _userService; private readonly WebRoutingSettings _webRoutingSettings; - private const int FailedLoginDurationRandomOffsetInMilliseconds = 100; private static long? _loginDurationAverage; // TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here @@ -419,13 +418,12 @@ public async Task IsAuthenticated() public async Task> PostLogin(LoginModel loginModel) { // Start a timed scope to ensure failed responses return is a consistent time - await using var timedScope = new TimedScope(GetLoginDuration(), CancellationToken.None); + var loginDuration = Math.Max(_loginDurationAverage ?? _securitySettings.UserDefaultFailedLoginDurationInMilliseconds, _securitySettings.UserMinimumFailedLoginDurationInMilliseconds); + await using var timedScope = new TimedScope(loginDuration, HttpContext.RequestAborted); // Sign the user in with username/password, this also gives a chance for developers to // custom verify the credentials and auto-link user accounts with a custom IBackOfficePasswordChecker - SignInResult result = await _signInManager.PasswordSignInAsync( - loginModel.Username, loginModel.Password, true, true); - + SignInResult result = await _signInManager.PasswordSignInAsync(loginModel.Username, loginModel.Password, true, true); if (result.Succeeded is false) { BackOfficeIdentityUser? user = await _userManager.FindByNameAsync(loginModel.Username.Trim()); @@ -476,22 +474,6 @@ await _userManager.CheckPasswordAsync(user, loginModel.Password)) return GetUserDetail(_userService.GetByUsername(loginModel.Username)); } - private long GetLoginDuration() - { - var loginDuration = Math.Max(_loginDurationAverage ?? _securitySettings.UserDefaultFailedLoginDurationInMilliseconds, _securitySettings.UserMinimumFailedLoginDurationInMilliseconds); - var random = new Random(); - var randomDelay = random.Next(-FailedLoginDurationRandomOffsetInMilliseconds, FailedLoginDurationRandomOffsetInMilliseconds); - loginDuration += randomDelay; - - // Just be sure we don't get a negative number - possible if someone has configured a very low UserMinimumFailedLoginDurationInMilliseconds value. - if (loginDuration < 0) - { - loginDuration = 0; - } - - return loginDuration; - } - /// /// Processes a password reset request. Looks for a match on the provided email address /// and if found sends an email with a link to reset it