X.Web.Lockout
1.0.1
Prefix Reserved
dotnet add package X.Web.Lockout --version 1.0.1
NuGet\Install-Package X.Web.Lockout -Version 1.0.1
<PackageReference Include="X.Web.Lockout" Version="1.0.1" />
<PackageVersion Include="X.Web.Lockout" Version="1.0.1" />
<PackageReference Include="X.Web.Lockout" />
paket add X.Web.Lockout --version 1.0.1
#r "nuget: X.Web.Lockout, 1.0.1"
#:package X.Web.Lockout@1.0.1
#addin nuget:?package=X.Web.Lockout&version=1.0.1
#tool nuget:?package=X.Web.Lockout&version=1.0.1
X.Web.Lockout
A small .NET library that adds user account lockout functionality on top of ASP.NET Core Identity (Microsoft.Extensions.Identity.Core).
Pick a backing store that fits your application — an in-process dictionary, IMemoryCache, IDistributedCache (Redis, SQL Server, etc.), or your existing IUserLockoutStore<TUser> — and lock out users after too many failed authentication attempts.
Features
- Two simple abstractions:
ILockoutService(keyed byuserId) andIUserLockoutService<TUser>(keyed by user instance) - Multiple backing implementations:
LockoutService—ConcurrentDictionary(single-instance, no extra dependencies)MemoryLockoutService—IMemoryCache(single-instance, with cache-managed eviction)DistributedLockoutService—IDistributedCache(multi-instance, Redis/SQL/etc.)StoreLockoutService/StoreUserLockoutService— wrap an existingIUserLockoutStore<TUser>
IUserLockoutStore<TUser>decorators (UserLockoutStore,DistributedUserLockoutStore) for plugging into ASP.NET Identity'sUserManagerpipeline- Auto-eviction of expired entries in the cache-based services
TimeProviderinjection for testable, deterministic time-based behavior- Reuses
Microsoft.AspNetCore.Identity.LockoutOptions(MaxFailedAccessAttempts,DefaultLockoutTimeSpan) — no custom config
Install
dotnet add package X.Web.Lockout
Quick start
1. Pick an implementation and register it
In-memory dictionary (simplest, single-instance):
using Microsoft.AspNetCore.Identity;
using X.Web.Lockout;
using X.Web.Lockout.Services;
var lockoutOptions = new LockoutOptions
{
MaxFailedAccessAttempts = 5,
DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15)
};
builder.Services.AddSingleton(lockoutOptions);
builder.Services.AddSingleton<ILockoutService, LockoutService>();
IMemoryCache-backed (single-instance, with eviction):
builder.Services.AddMemoryCache();
builder.Services.AddSingleton(lockoutOptions);
builder.Services.AddSingleton<ILockoutService, MemoryLockoutService>();
IDistributedCache-backed (multi-instance, Redis / SQL Server / etc.):
builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379");
builder.Services.AddSingleton(lockoutOptions);
builder.Services.AddSingleton<ILockoutService, DistributedLockoutService>();
2. Use it in your authentication flow
public class LoginHandler
{
private readonly ILockoutService _lockout;
public LoginHandler(ILockoutService lockout) => _lockout = lockout;
public async Task<bool> SignInAsync(string userId, string password)
{
if (await _lockout.GetLockoutEnabledAsync(userId))
{
throw new InvalidOperationException("Account is temporarily locked.");
}
if (!ValidatePassword(userId, password))
{
await _lockout.IncrementAccessFailedCountAsync(userId);
return false;
}
await _lockout.ResetAccessFailedCountAsync(userId);
return true;
}
}
API
ILockoutService
public interface ILockoutService
{
Task<bool> GetLockoutEnabledAsync(string userId, CancellationToken cancellationToken = default);
Task IncrementAccessFailedCountAsync(string userId, CancellationToken cancellationToken = default);
Task ResetAccessFailedCountAsync(string userId, CancellationToken cancellationToken = default);
}
IUserLockoutService<TUser>
Strongly-typed variant for when you already have a user instance and don't want to look up by id:
public interface IUserLockoutService<in TUser> where TUser : class
{
Task<bool> GetLockoutEnabledAsync(TUser user, CancellationToken cancellationToken = default);
Task IncrementAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default);
Task ResetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default);
}
Behavior
- Failure tracking —
IncrementAccessFailedCountAsyncincrements a per-user counter. When it reachesLockoutOptions.MaxFailedAccessAttempts, the user is locked out forLockoutOptions.DefaultLockoutTimeSpan. - Lockout check —
GetLockoutEnabledAsyncreturnstrueonly while the lockout window is in the future. Past lockouts are treated as inactive. - Reset —
ResetAccessFailedCountAsyncclears both the failure counter and the lockout end date. Call it on a successful authentication. - Sliding window for failed attempts — for the cache-based services (
MemoryLockoutService,DistributedLockoutService), each failed attempt extends the entry lifetime byDefaultLockoutTimeSpan. An attacker who pauses longer than that window starts from a clean slate. - Self-eviction — when a user is locked out, the cache entry's absolute expiration is set to the remaining lockout time, so expired lockouts are removed automatically without any cleanup job.
Implementations
| Service | Backing | When to use |
|---|---|---|
LockoutService |
ConcurrentDictionary |
Quick start, single-instance apps. State lost on restart. No automatic eviction — entries grow unbounded; not recommended for production with untrusted user ids. |
MemoryLockoutService |
IMemoryCache |
Single-instance apps with auto-eviction. Configure SizeLimit on MemoryCacheOptions to bound memory. |
DistributedLockoutService |
IDistributedCache |
Multi-instance / load-balanced apps. Works with any IDistributedCache provider (Redis, SQL Server, NCache, etc.). |
StoreLockoutService<TUser> |
IUserLockoutStore<TUser> |
When lockout state should live in your existing user table (Entity Framework, Dapper, etc.). |
StoreUserLockoutService<TUser> |
IUserLockoutStore<TUser> |
Same as above, but takes the TUser instance directly (no FindByIdAsync lookup). |
IUserLockoutStore<TUser> decorators
If you want to plug lockout state into ASP.NET Identity's UserManager<TUser> pipeline, use one of the included decorators. Each wraps an inner IUserStore<TUser> and adds lockout-specific storage:
UserLockoutStore<TUser>— keeps lockout state inConcurrentDictionaryDistributedUserLockoutStore<TUser>— keeps lockout state inIDistributedCache
Both delegate user CRUD (CreateAsync, FindByIdAsync, etc.) to the inner store.
Testability
Every service accepts a TimeProvider via constructor injection. Combined with Microsoft.Extensions.TimeProvider.Testing's FakeTimeProvider, lockout windows and expirations can be tested deterministically:
var time = new FakeTimeProvider();
var service = new LockoutService(options, time);
await service.IncrementAccessFailedCountAsync("user1");
// ...
time.Advance(TimeSpan.FromMinutes(16));
Assert.False(await service.GetLockoutEnabledAsync("user1"));
Build & test
dotnet build X.Web.Lockout.slnx
dotnet test X.Web.Lockout.slnx
License
Apache-2.0 — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Bcl.TimeProvider (>= 10.0.5)
- Microsoft.Extensions.Caching.Abstractions (>= 10.0.5)
- Microsoft.Extensions.Caching.Memory (>= 10.0.5)
- Microsoft.Extensions.Identity.Core (>= 10.0.5)
- System.Text.Json (>= 10.0.5)
-
net10.0
- Microsoft.Extensions.Caching.Abstractions (>= 10.0.5)
- Microsoft.Extensions.Caching.Memory (>= 10.0.5)
- Microsoft.Extensions.Identity.Core (>= 10.0.5)
-
net8.0
- Microsoft.Extensions.Caching.Abstractions (>= 10.0.5)
- Microsoft.Extensions.Caching.Memory (>= 10.0.5)
- Microsoft.Extensions.Identity.Core (>= 10.0.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.