4

I'm using AspNetCoreRateLimit library with Asp.Net Core 2.2 web api. I've taken IpRateLimiting into use with it's default settings in Startup.cs as seen AspNetCoreRateLimit wiki.

I have API endpoint with query parameters, and it is used with http GET queries as following (see parameters startDate and stopDate):

GET "https://host/api/endpoint/path?startDate=2020-04-04&stopDate=2020-04-04"

I want to limit only unique requests (with unique parameter combinations) to 5 requests per hour. So, for example, the following scenario should be possible in 1 hour:

5 times: GET "https://host/api/endpoint/path?startDate=2020-04-04&stopDate=2020-04-04"
5 times: GET "https://host/api/endpoint/path?startDate=2020-04-05&stopDate=2020-04-05"

The problem is that I can send only total 5 requests per hour regardless of parameters.

Following is my IpRateLimiting Settings from appsettings.json.

"IpRateLimiting": {
    "EnableEndpointRateLimiting": true,
    "StackBlockedRequests": false,
    "RealIPHeader": "X-Real-IP",
    "ClientIdHeader": "X-ClientId",
    "HttpStatusCode": 429,
    "GeneralRules": [
      {
        "Endpoint": "*:/api/endpoint/path",
        "Period": "1h",
        "Limit": 5
      }
    ]
  }

Please note, that I don't want to change the endpoint route as proposed in this good answer by @Yongqing Yu, because there are a bunch of API clients out there using my API and I do not want to introduce any breaking changes.

Risto M
  • 2,919
  • 1
  • 14
  • 27

2 Answers2

4

You can change the route of the corresponding action and turn the parameter directly into a part of the path like 'https://host/api/endpoint/path/2020-04-04/2020-04-04', so that the Endpoint in GeneralRules can satisfy your condition by *.

You can refer to this.

Here is my demo:

[Route("api/[controller]")]
[ApiController]
public class DefaultController : ControllerBase
{
    [HttpGet("Test/{startDate}/{stopDate}")]
    public string Test(string startDate, string stopDate)
    {
        return "Ok";
    }
}

appsettings.json:

"IpRateLimiting": {
        "EnableEndpointRateLimiting": true,
        "StackBlockedRequests": false,
        "RealIPHeader": "X-Real-IP",
        "ClientIdHeader": "X-ClientId",
        "HttpStatusCode": 429,
        "GeneralRules": [
          {
            "Endpoint": "*:/api/default/Test/*",
            "Period": "1h",
            "Limit": 5
          }
        ]
      }

Here is the test result:

enter image description here

LouraQ
  • 6,443
  • 2
  • 6
  • 16
  • Thanks for your good answer. In my case changing controller method signature is a bit problematic because there are a bunch of API clients using my endpoint so I do not want to introduce any breaking changes.. I have to take a look if I can create a custom resolver. – Risto M May 14 '20 at 08:21
3

I found a solution and thus answering to myself. In my case I am not able to change the controller method route as proposed in another answer.

As mentioned here it is possible to implement own path extraction logic. I wrote custom IpRateLimitMiddleware and overrided ResolveIdentity-method as follows:

public class CustomIpRateLimitMiddleware : IpRateLimitMiddleware
{
    private readonly ILogger<CustomIpRateLimitMiddleware> _logger;
    private readonly IRateLimitConfiguration _config;

    public CustomIpRateLimitMiddleware(RequestDelegate next,
        IOptions<IpRateLimitOptions> options,
        IRateLimitCounterStore counterStore,
        IIpPolicyStore policyStore,
        IRateLimitConfiguration config,
        ILogger<CustomIpRateLimitMiddleware> logger)
    : base(next, options, counterStore, policyStore, config, logger)

    {
        _config = config;
        _logger = logger;
    }

    public override ClientRequestIdentity ResolveIdentity(HttpContext httpContext)
    {
        var identity = base.ResolveIdentity(httpContext);

        if (httpContext.Request.Query == null && !httpContext.Request.Query.Any())
        {
            return identity;
        }

        StringBuilder path = new StringBuilder(httpContext.Request.Path.ToString().ToLowerInvariant());
        foreach (var parameter in httpContext.Request.Query)
        {
            path.Append("/" + parameter.Value);
        }
        identity.Path = path.ToString();
        return identity;
    }
}

And this is initialized in Startup.cs as following:

ConfigureServices-method:

services.AddOptions();
services.AddMemoryCache();
services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();

Configure-method:

app.UseMiddleware<CustomIpRateLimitMiddleware>();

The code snippet above amends query for the middleware so it looks like those query parameters are part of the path.

So instead of this:

/api/endpoint/path?startDate=2020-04-04&stopDate=2020-04-04"

AspNetCoreRateLimit is getting path as following format:

/api/endpoint/path/2020-04-04/2020-04-04

..and now my rate limiting configuration can be this:

"IpRateLimiting": {
    "EnableEndpointRateLimiting": true,
    "StackBlockedRequests": false,
    "RealIPHeader": "X-Real-IP",
    "ClientIdHeader": "X-ClientId",
    "HttpStatusCode": 429,
    "GeneralRules": [
      {
        "Endpoint": "*:/api/path/*",
        "Period": "1h",
        "Limit": 5
      }
    ]
  }
Risto M
  • 2,919
  • 1
  • 14
  • 27
  • 1
    And how did you use the Custom middleware, did you register the middleware on StartUp? – Danny Jun 11 '20 at 10:37
  • Thanks for the update, I imagine you replaced this line app.UseIpRateLimiting(); with this one app.UseMiddleware(); – Danny Jun 11 '20 at 13:26
  • 1
    Thats right, since I made my own Middleware. There is now that whole class CustomIpRateLimitMiddleware in my answer. – Risto M Jun 11 '20 at 13:27