Skip to content
6 changes: 6 additions & 0 deletions src/Identity/Extensions.Core/src/LockoutOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,10 @@ public class LockoutOptions
/// </summary>
/// <value>The <see cref="TimeSpan"/> a user is locked out for when a lockout occurs.</value>
public TimeSpan DefaultLockoutTimeSpan { get; set; } = TimeSpan.FromMinutes(5);

/// <summary>
/// Specifies whether the lockout should be permanent.
/// If true, the user will be locked out indefinitely.
/// </summary>
public bool PermanentLockout { get; set; } = false;
}
45 changes: 30 additions & 15 deletions src/Identity/Extensions.Core/src/UserManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1812,24 +1812,39 @@ public virtual async Task<IdentityResult> SetLockoutEndDateAsync(TUser user, Dat
/// </summary>
/// <param name="user">The user whose failed access count to increment.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the operation.</returns>
public virtual async Task<IdentityResult> AccessFailedAsync(TUser user)
{
ThrowIfDisposed();
var store = GetUserLockoutStore();
ArgumentNullThrowHelper.ThrowIfNull(user);
public virtual async Task<IdentityResult> AccessFailedAsync(TUser user)
{
ThrowIfDisposed();
var store = GetUserLockoutStore();
ArgumentNullThrowHelper.ThrowIfNull(user);

// If this puts the user over the threshold for lockout, lock them out and reset the access failed count
var count = await store.IncrementAccessFailedCountAsync(user, CancellationToken).ConfigureAwait(false);
if (count < Options.Lockout.MaxFailedAccessAttempts)
{
return await UpdateUserAsync(user).ConfigureAwait(false);
}
Logger.LogDebug(LoggerEventIds.UserLockedOut, "User is locked out.");
await store.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan),
CancellationToken).ConfigureAwait(false);
await store.ResetAccessFailedCountAsync(user, CancellationToken).ConfigureAwait(false);
// If this puts the user over the threshold for lockout, lock them out and reset the access failed count
var count = await store.IncrementAccessFailedCountAsync(user, CancellationToken).ConfigureAwait(false);
if (count < Options.Lockout.MaxFailedAccessAttempts)
{
return await UpdateUserAsync(user).ConfigureAwait(false);
}
Logger.LogDebug(LoggerEventIds.UserLockedOut, "User is locked out.");

// Prevent overflow when setting lockout end date
var now = DateTimeOffset.UtcNow;
DateTimeOffset lockoutEnd;

if (Options.Lockout.DefaultLockoutTimeSpan == TimeSpan.MaxValue)
{
lockoutEnd = DateTimeOffset.MaxValue;
}
else
{
lockoutEnd = now > (DateTimeOffset.MaxValue - Options.Lockout.DefaultLockoutTimeSpan)
? DateTimeOffset.MaxValue
: now.Add(Options.Lockout.DefaultLockoutTimeSpan);
}

await store.SetLockoutEndDateAsync(user, lockoutEnd, CancellationToken).ConfigureAwait(false);
await store.ResetAccessFailedCountAsync(user, CancellationToken).ConfigureAwait(false);
return await UpdateUserAsync(user).ConfigureAwait(false);
}

/// <summary>
/// Resets the access failed count for the specified <paramref name="user"/>.
Expand Down
18 changes: 18 additions & 0 deletions src/Identity/test/Identity.Test/UserManagerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,24 @@ public async Task ResetTokenCallNoopForTokenValueZero()
IdentityResultAssert.IsSuccess(await manager.ResetAccessFailedCountAsync(user));
}

[Fact]
public async Task AccessFailedAsync_IncrementsAccessFailedCount()
{
// Arrange
var user = new PocoUser() { UserName = "testuser" };
var store = new Mock<IUserLockoutStore<PocoUser>>();
store.Setup(x => x.GetAccessFailedCountAsync(user, It.IsAny<CancellationToken>())).ReturnsAsync(1);
store.Setup(x => x.IncrementAccessFailedCountAsync(user, It.IsAny<CancellationToken>())).ReturnsAsync(2);

var manager = MockHelpers.TestUserManager(store.Object);

// Act
var result = await manager.AccessFailedAsync(user);

// Assert
Assert.Equal(2, result);
}

[Fact]
public async Task ManagerPublicNullChecks()
{
Expand Down
Loading