Skip to content

Commit 65bb280

Browse files
ronaldbarendseZeegaan
authored andcommitted
Merge commit from fork
* Add TimedScope * Use TimedScope in login endpoint * Use seperate default duration and only calculate average of actual successful responses * Only return detailed error responses if credentials are valid * Cancel timed scope when credentials are valid * Add UserDefaultFailedLoginDuration and UserMinimumFailedLoginDuration settings
1 parent a0a4af6 commit 65bb280

File tree

3 files changed

+190
-1
lines changed

3 files changed

+190
-1
lines changed

src/Umbraco.Core/Configuration/Models/SecuritySettings.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// See LICENSE for more details.
33

44
using System.ComponentModel;
5+
using System.ComponentModel.DataAnnotations;
56

67
namespace Umbraco.Cms.Core.Configuration.Models;
78

@@ -25,6 +26,8 @@ public class SecuritySettings
2526

2627
internal const int StaticMemberDefaultLockoutTimeInMinutes = 30 * 24 * 60;
2728
internal const int StaticUserDefaultLockoutTimeInMinutes = 30 * 24 * 60;
29+
private const long StaticUserDefaultFailedLoginDurationInMilliseconds = 1000;
30+
private const long StaticUserMinimumFailedLoginDurationInMilliseconds = 250;
2831
internal const string StaticAuthorizeCallbackPathName = "/umbraco/oauth_complete";
2932
internal const string StaticAuthorizeCallbackLogoutPathName = "/umbraco/logout";
3033
internal const string StaticAuthorizeCallbackErrorPathName = "/umbraco/error";
@@ -108,6 +111,30 @@ public class SecuritySettings
108111
[DefaultValue(StaticAllowConcurrentLogins)]
109112
public bool AllowConcurrentLogins { get; set; } = StaticAllowConcurrentLogins;
110113

114+
/// <summary>
115+
/// Gets or sets the default duration (in milliseconds) of failed login attempts.
116+
/// </summary>
117+
/// <value>
118+
/// The default duration (in milliseconds) of failed login attempts.
119+
/// </value>
120+
/// <remarks>
121+
/// The user login endpoint ensures that failed login attempts take at least as long as the average successful login.
122+
/// However, if no successful logins have occurred, this value is used as the default duration.
123+
/// </remarks>
124+
[Range(0, long.MaxValue)]
125+
[DefaultValue(StaticUserDefaultFailedLoginDurationInMilliseconds)]
126+
public long UserDefaultFailedLoginDurationInMilliseconds { get; set; } = StaticUserDefaultFailedLoginDurationInMilliseconds;
127+
128+
/// <summary>
129+
/// Gets or sets the minimum duration (in milliseconds) of failed login attempts.
130+
/// </summary>
131+
/// <value>
132+
/// The minimum duration (in milliseconds) of failed login attempts.
133+
/// </value>
134+
[Range(0, long.MaxValue)]
135+
[DefaultValue(StaticUserMinimumFailedLoginDurationInMilliseconds)]
136+
public long UserMinimumFailedLoginDurationInMilliseconds { get; set; } = StaticUserMinimumFailedLoginDurationInMilliseconds;
137+
111138
/// <summary>
112139
/// Gets or sets a value of the back-office host URI. Use this when running the back-office client and the Management API on different hosts. Leave empty when running both on the same host.
113140
/// </summary>

src/Umbraco.Core/TimedScope.cs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
namespace Umbraco.Cms.Core;
2+
3+
/// <summary>
4+
/// Makes a code block timed (take at least a certain amount of time). This class cannot be inherited.
5+
/// </summary>
6+
public sealed class TimedScope : IDisposable, IAsyncDisposable
7+
{
8+
private readonly TimeSpan _duration;
9+
private readonly TimeProvider _timeProvider;
10+
private readonly CancellationTokenSource _cancellationTokenSource;
11+
private readonly long _startingTimestamp;
12+
13+
/// <summary>
14+
/// Gets the elapsed time.
15+
/// </summary>
16+
/// <value>
17+
/// The elapsed time.
18+
/// </value>
19+
public TimeSpan Elapsed
20+
=> _timeProvider.GetElapsedTime(_startingTimestamp);
21+
22+
/// <summary>
23+
/// Gets the remaining time.
24+
/// </summary>
25+
/// <value>
26+
/// The remaining time.
27+
/// </value>
28+
public TimeSpan Remaining
29+
=> TryGetRemaining(out TimeSpan remaining) ? remaining : TimeSpan.Zero;
30+
31+
/// <summary>
32+
/// Initializes a new instance of the <see cref="TimedScope" /> class.
33+
/// </summary>
34+
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
35+
public TimedScope(long millisecondsDuration)
36+
: this(TimeSpan.FromMilliseconds(millisecondsDuration))
37+
{ }
38+
39+
/// <summary>
40+
/// Initializes a new instance of the <see cref="TimedScope" /> class.
41+
/// </summary>
42+
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
43+
/// <param name="cancellationToken">The cancellation token.</param>
44+
public TimedScope(long millisecondsDuration, CancellationToken cancellationToken)
45+
: this(TimeSpan.FromMilliseconds(millisecondsDuration), cancellationToken)
46+
{ }
47+
48+
/// <summary>
49+
/// Initializes a new instance of the <see cref="TimedScope" /> class.
50+
/// </summary>
51+
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
52+
/// <param name="timeProvider">The time provider.</param>
53+
public TimedScope(long millisecondsDuration, TimeProvider timeProvider)
54+
: this(TimeSpan.FromMilliseconds(millisecondsDuration), timeProvider)
55+
{ }
56+
57+
/// <summary>
58+
/// Initializes a new instance of the <see cref="TimedScope" /> class.
59+
/// </summary>
60+
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
61+
/// <param name="timeProvider">The time provider.</param>
62+
/// <param name="cancellationToken">The cancellation token.</param>
63+
public TimedScope(long millisecondsDuration, TimeProvider timeProvider, CancellationToken cancellationToken)
64+
: this(TimeSpan.FromMilliseconds(millisecondsDuration), timeProvider, cancellationToken)
65+
{ }
66+
67+
/// <summary>
68+
/// Initializes a new instance of the <see cref="TimedScope"/> class.
69+
/// </summary>
70+
/// <param name="duration">The duration the scope should at least take.</param>
71+
public TimedScope(TimeSpan duration)
72+
: this(duration, TimeProvider.System)
73+
{ }
74+
75+
/// <summary>
76+
/// Initializes a new instance of the <see cref="TimedScope" /> class.
77+
/// </summary>
78+
/// <param name="duration">The duration the scope should at least take.</param>
79+
/// <param name="timeProvider">The time provider.</param>
80+
public TimedScope(TimeSpan duration, TimeProvider timeProvider)
81+
: this(duration, timeProvider, new CancellationTokenSource())
82+
{ }
83+
84+
/// <summary>
85+
/// Initializes a new instance of the <see cref="TimedScope" /> class.
86+
/// </summary>
87+
/// <param name="duration">The duration the scope should at least take.</param>
88+
/// <param name="cancellationToken">The cancellation token.</param>
89+
public TimedScope(TimeSpan duration, CancellationToken cancellationToken)
90+
: this(duration, TimeProvider.System, cancellationToken)
91+
{ }
92+
93+
/// <summary>
94+
/// Initializes a new instance of the <see cref="TimedScope" /> class.
95+
/// </summary>
96+
/// <param name="duration">The duration the scope should at least take.</param>
97+
/// <param name="timeProvider">The time provider.</param>
98+
/// <param name="cancellationToken">The cancellation token.</param>
99+
public TimedScope(TimeSpan duration, TimeProvider timeProvider, CancellationToken cancellationToken)
100+
: this(duration, timeProvider, CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
101+
{ }
102+
103+
private TimedScope(TimeSpan duration, TimeProvider timeProvider, CancellationTokenSource cancellationTokenSource)
104+
{
105+
_duration = duration;
106+
_timeProvider = timeProvider;
107+
_cancellationTokenSource = cancellationTokenSource;
108+
_startingTimestamp = timeProvider.GetTimestamp();
109+
}
110+
111+
/// <summary>
112+
/// Cancels the timed scope.
113+
/// </summary>
114+
public void Cancel()
115+
=> _cancellationTokenSource.Cancel();
116+
117+
/// <summary>
118+
/// Cancels the timed scope asynchronously.
119+
/// </summary>
120+
public async Task CancelAsync()
121+
=> await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
122+
123+
/// <summary>
124+
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
125+
/// </summary>
126+
/// <remarks>
127+
/// This will block using <see cref="Thread.Sleep(TimeSpan)" /> until the remaining time has elapsed, if not cancelled.
128+
/// </remarks>
129+
public void Dispose()
130+
{
131+
if (_cancellationTokenSource.IsCancellationRequested is false &&
132+
TryGetRemaining(out TimeSpan remaining))
133+
{
134+
Thread.Sleep(remaining);
135+
}
136+
}
137+
138+
/// <summary>
139+
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously.
140+
/// </summary>
141+
/// <returns>
142+
/// A task that represents the asynchronous dispose operation.
143+
/// </returns>
144+
/// <remarks>
145+
/// This will delay using <see cref="Task.Delay(TimeSpan, TimeProvider, CancellationToken)" /> until the remaining time has elapsed, if not cancelled.
146+
/// </remarks>
147+
public async ValueTask DisposeAsync()
148+
{
149+
if (_cancellationTokenSource.IsCancellationRequested is false &&
150+
TryGetRemaining(out TimeSpan remaining))
151+
{
152+
await Task.Delay(remaining, _timeProvider, _cancellationTokenSource.Token).ConfigureAwait(false);
153+
}
154+
}
155+
156+
private bool TryGetRemaining(out TimeSpan remaining)
157+
{
158+
remaining = _duration.Subtract(Elapsed);
159+
160+
return remaining > TimeSpan.Zero;
161+
}
162+
}

0 commit comments

Comments
 (0)