Adding Distributed Rate Limiting to ASP.NET Identity Pages

It’s generally a best practice to tighten security on your login, register and forgot password pages as these will be common targets for bad actors.

In my Minimal Identity UI project I have already implemented Altcha (a work based privacy aware challenge solution) that reduces the chances of automated attacks. Another approach (which can go along side a challenge) is rate limiting which ASP.NET introduced in version 8.

By default though the built-in rate limited is not distributed so that if you use Azure App Services (or a web server cluster) to host your app, each server will be rate limiting individually which can cause the limits to double or triple (depending on the number of machines in the cluster). Sticky sessions can be an option to mitigate this, but for Minimal Identity UI I decided to opt for a distributed rate limiter based on Redis provided by cristipufu.

This library integrates into the standard rate limited hardware, but uses Redis as a backplane to ensure all servers pointing to the same Redis server act as one with regard to the rate limiting policies.

A full implementation of best practice policies for ASP.NET Identity rate limiting can be found here, but let’s look at the important implementation parts in detail.

Implementing distributed rate limiting in ASP.NET

The main part of the configuration is done in the Program.cs file. We add the rate limiter services and change the default status code to 429 to better align with common practice on the web.

builder.Services.AddRateLimiter(options =>
{
   // Switch to standard 429 response, default is 503
   options.RejectionStatusCode = 429;

   // Policies go here
}

Add any number of policies you would like, for example this configures a sliding window rate limit of 10 requests per minute using the Redis based rate limiter.

// Policy for login and register endpoints: rate limited to 10 requests per minute
options.AddPolicy(RateLimiterPolicy.LoginAndRegister, httpContext =>
    RedisRateLimitPartition.GetSlidingWindowRateLimiter(
        partitionKey:
            $"loginreg-{httpContext.Connection.RemoteIpAddress}",
        factory: _ => new RedisSlidingWindowRateLimiterOptions
        {
            ConnectionMultiplexerFactory = () => connectionMultiplexer,
            PermitLimit = 10,
            Window = TimeSpan.FromMinutes(1)
        }));

The connectionMultiplexer is a standard StackExchange.Redis client initialization:

var connectionMultiplexer = ConnectionMultiplexer.Connect(
  builder.Configuration.GetConnectionString("Redis") ??
    throw new InvalidOperationException("'Redis' value not found."));

To enable the rate limiting middleware add it after UseRouting and we also add UseForwardedHeaders to allow any proxied IP address to be read in the HttpContext RemoteIPAddress field used in the above policy.

app.UseRouting();
app.UseForwardedHeaders();
app.UseRateLimiter();
app.UseAuthorization();
app.MapRazorPages();
app.Run();

Using a custom extension method class we can then add the rate limiter policies using page conventions to the area pages (or other pages/folders) in the AddRazorPages configuration block:

builder.Services.AddRazorPages(options =>
{
  options.Conventions.RateLimitAreaPage(
    "Identity",
    "/Account/Register",
    "policy-name");
});

To sum up you can now rate limit any page or folder in your ASP.NET project.

Best Practices for rate limiting ASP.NET Identity

For MinimalIdentityUI the following policies were used:

Rate Limiting Considerations for ASP.NET

Rate limiting in the ASP.NET apps itself should not be the only form of rate limiting, but can be used in conjungtion.

In general rate limiting at the gateway level through services such as Azure API Management or Cloudflare is recommended as they are better equipped to handle potential attack on different network levels and can monitor not just .NET requests but all requests coming into your servers.

But ASP.NET rate limiting can be useful if you want to rate limit based on certain application aspects like user name, email domain, api token etc. This can generally be harder to implement at the gateway level.