Skip to content
Open
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 is locked out.
/// </summary>
public bool PermanentLockout { get; set; }
}
2 changes: 2 additions & 0 deletions src/Identity/Extensions.Core/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#nullable enable
*REMOVED*Microsoft.AspNetCore.Identity.UserLoginInfo.UserLoginInfo(string! loginProvider, string! providerKey, string? displayName) -> void
Microsoft.AspNetCore.Identity.UserLoginInfo.UserLoginInfo(string! loginProvider, string! providerKey, string? providerDisplayName) -> void
Microsoft.AspNetCore.Identity.LockoutOptions.PermanentLockout.get -> bool
Microsoft.AspNetCore.Identity.LockoutOptions.PermanentLockout.set -> void
21 changes: 18 additions & 3 deletions src/Identity/Extensions.Core/src/UserManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1818,15 +1818,30 @@ public virtual async Task<IdentityResult> AccessFailedAsync(TUser user)
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
// If PermanentLockout is enabled, lock the user indefinitely
if (Options.Lockout.PermanentLockout)
{
Logger.LogDebug(LoggerEventIds.UserLockedOut, "User is permanently locked out.");
await store.SetLockoutEndDateAsync(user, DateTimeOffset.MaxValue, CancellationToken).ConfigureAwait(false);
return await UpdateUserAsync(user).ConfigureAwait(false);
}

// Increment 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);

// Set the lockout time based on configuration.
var now = DateTimeOffset.UtcNow;
DateTimeOffset lockoutEnd = Options.Lockout.DefaultLockoutTimeSpan == TimeSpan.MaxValue
? 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);
}
Expand Down
27 changes: 27 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,33 @@ public async Task ResetTokenCallNoopForTokenValueZero()
IdentityResultAssert.IsSuccess(await manager.ResetAccessFailedCountAsync(user));
}

[Fact]
public async Task AccessFailedAsyncIncrementsAccessFailedCount()
{
// Arrange
var user = new PocoUser() { UserName = "testuser" };
var store = new Mock<IUserLockoutStore<PocoUser>>();
int failedCount = 1; // Simulated access failed count

store.Setup(x => x.GetAccessFailedCountAsync(user, It.IsAny<CancellationToken>()))
.ReturnsAsync(() => failedCount); // Return the updated value dynamically

store.Setup(x => x.IncrementAccessFailedCountAsync(user, It.IsAny<CancellationToken>()))
.ReturnsAsync(() => ++failedCount); // Increment and return the new count

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

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

// Assert
IdentityResultAssert.IsSuccess(result);
store.Verify(x => x.IncrementAccessFailedCountAsync(user, It.IsAny<CancellationToken>()), Times.Once);

var newFailedCount = await manager.GetAccessFailedCountAsync(user);
Assert.Equal(2, newFailedCount); // Ensure the count was actually updated
}

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