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