Skip to content

Commit a0a4af6

Browse files
authored
Merge commit from fork
1 parent e8d6cde commit a0a4af6

File tree

1 file changed

+54
-32
lines changed

1 file changed

+54
-32
lines changed

src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ namespace Umbraco.Cms.Api.Management.Controllers.Security;
3434
[ApiExplorerSettings(IgnoreApi = true)]
3535
public class BackOfficeController : SecurityControllerBase
3636
{
37+
private static long? _loginDurationAverage;
38+
3739
private readonly IHttpContextAccessor _httpContextAccessor;
3840
private readonly IBackOfficeSignInManager _backOfficeSignInManager;
3941
private readonly IBackOfficeUserManager _backOfficeUserManager;
@@ -72,45 +74,65 @@ public BackOfficeController(
7274
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
7375
public async Task<IActionResult> Login(CancellationToken cancellationToken, LoginRequestModel model)
7476
{
75-
IdentitySignInResult result = await _backOfficeSignInManager.PasswordSignInAsync(
76-
model.Username, model.Password, true, true);
77+
// Start a timed scope to ensure failed responses return is a consistent time
78+
var loginDuration = Math.Max(_loginDurationAverage ?? _securitySettings.Value.UserDefaultFailedLoginDurationInMilliseconds, _securitySettings.Value.UserMinimumFailedLoginDurationInMilliseconds);
79+
await using var timedScope = new TimedScope(loginDuration, cancellationToken);
7780

78-
if (result.IsNotAllowed)
81+
IdentitySignInResult result = await _backOfficeSignInManager.PasswordSignInAsync(model.Username, model.Password, true, true);
82+
if (result.Succeeded is false)
7983
{
80-
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetailsBuilder()
81-
.WithTitle("User is not allowed")
82-
.WithDetail("The operation is not allowed on the user")
83-
.Build());
84-
}
84+
// TODO: The result should include the user and whether the credentials were valid to avoid these additional checks
85+
BackOfficeIdentityUser? user = await _backOfficeUserManager.FindByNameAsync(model.Username.Trim()); // Align with UmbracoSignInManager and trim username!
86+
if (user is not null &&
87+
await _backOfficeUserManager.CheckPasswordAsync(user, model.Password))
88+
{
89+
// The credentials were correct, so cancel timed scope and provide a more detailed failure response
90+
await timedScope.CancelAsync();
91+
92+
if (result.IsNotAllowed)
93+
{
94+
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetailsBuilder()
95+
.WithTitle("User is not allowed")
96+
.WithDetail("The operation is not allowed on the user")
97+
.Build());
98+
}
99+
100+
if (result.IsLockedOut)
101+
{
102+
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetailsBuilder()
103+
.WithTitle("User is locked")
104+
.WithDetail("The user is locked, and need to be unlocked before more login attempts can be executed.")
105+
.Build());
106+
}
107+
108+
if (result.RequiresTwoFactor)
109+
{
110+
string? twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(model.Username);
111+
IEnumerable<string> enabledProviders = (await _userTwoFactorLoginService.GetProviderNamesAsync(user.Key)).Result.Where(x => x.IsEnabledOnUser).Select(x => x.ProviderName);
112+
113+
return StatusCode(StatusCodes.Status402PaymentRequired, new RequiresTwoFactorResponseModel()
114+
{
115+
TwoFactorLoginView = twofactorView,
116+
EnabledTwoFactorProviderNames = enabledProviders
117+
});
118+
}
119+
}
85120

86-
if (result.IsLockedOut)
87-
{
88-
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetailsBuilder()
89-
.WithTitle("User is locked")
90-
.WithDetail("The user is locked, and need to be unlocked before more login attempts can be executed.")
121+
return StatusCode(StatusCodes.Status401Unauthorized, new ProblemDetailsBuilder()
122+
.WithTitle("Invalid credentials")
123+
.WithDetail("The provided credentials are invalid. User has not been signed in.")
91124
.Build());
92125
}
93126

94-
if(result.RequiresTwoFactor)
95-
{
96-
string? twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(model.Username);
97-
BackOfficeIdentityUser? attemptingUser = await _backOfficeUserManager.FindByNameAsync(model.Username);
98-
IEnumerable<string> enabledProviders = (await _userTwoFactorLoginService.GetProviderNamesAsync(attemptingUser!.Key)).Result.Where(x=>x.IsEnabledOnUser).Select(x=>x.ProviderName);
99-
return StatusCode(StatusCodes.Status402PaymentRequired, new RequiresTwoFactorResponseModel()
100-
{
101-
TwoFactorLoginView = twofactorView,
102-
EnabledTwoFactorProviderNames = enabledProviders
103-
});
104-
}
127+
// Set initial or update average (successful) login duration
128+
_loginDurationAverage = _loginDurationAverage is long average
129+
? (average + (long)timedScope.Elapsed.TotalMilliseconds) / 2
130+
: (long)timedScope.Elapsed.TotalMilliseconds;
105131

106-
if (result.Succeeded)
107-
{
108-
return Ok();
109-
}
110-
return StatusCode(StatusCodes.Status401Unauthorized, new ProblemDetailsBuilder()
111-
.WithTitle("Invalid credentials")
112-
.WithDetail("The provided credentials are invalid. User has not been signed in.")
113-
.Build());
132+
// Cancel the timed scope (we don't want to unnecessarily wait on a successful response)
133+
await timedScope.CancelAsync();
134+
135+
return Ok();
114136
}
115137

116138
[AllowAnonymous]

0 commit comments

Comments
 (0)