Adding Rate Limiting to Web API and Application

An application has a request for checking report status, post, messages, OTP, etc. If these requests don't have a rate limit set any malicious user can use this request and abuse these functionalities.

To prevent this in an Asp.net MVC web application we will implement an IP address based rate limit setter using Action Filters. Such that we can just decorate the action with the attribute on which we need to set the rate limit.

To implement this we have to do the following.

  1. Create a table to store IP address
  2. Get the IP address of client machine
  3. Validate IP address in the attribute filter
  4. Decorate the method with the attribute

1> Create a table to store IP address

First, we need to create a table in the database which would persistently store all the IP addresses that will consume the method or resource.

Table Structure

The table should contain at least 3 columns IPAddress, MethodName and CreatedOn to store the IP address of the client consuming the resource, the method or resource name to identify uniquely against which the log is added and lastly the date and time.

2> Get IP Address of Client Machine

The below-given method is used to get the IP address of the client. The GetIpAddress method consumes the object of the current request that is of type "HttpRequestBase" to get the IP of the client machine. 

public class Common
{
        public static string GetIpAddress(HttpRequestBase Request)
        {
            // Request.UserHostAddress returns the IP address of the client.
            var userip = Request.UserHostAddress;
            if (Request.UserHostAddress != null)
            {
                return userip;
            }
            //In case the Request.UserHostAddress returns null then we need to read the ip address from the ServerVariables
            else
            {
                //Request.ServerVariables["HTTP_X_FORWARDED_FOR"] will have value if the client machine is using a proxy server 
                userip = Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
                if (string.IsNullOrEmpty(userip))
                {
                    //Request.ServerVariables["REMOTE_ADDR"] contains the ip address of the client.
                    userip = Request.ServerVariables["REMOTE_ADDR"];
                }
            }
            return userip;
        }
}

 

3> Validate IP address in the attribute filter

Here I presume that you know how "action filters" work, if not please check out this link "Understanding Action Filters (C#)". Now we create an ActionFilterAttribute that will validate the IP address and prevent the execution of the method. This takes 3 arguments that are given below

a> RateLimitOnMethod

This argument takes a string as an input which is used to identify the method or resource on which the rate limit is set on. This is used to identify each resource uniquely and can set and can maintain different rate limits on different methods. 

b> RateLimit

This argument takes an integer as input and is optional, the default value of this argument is set to 3. This is used to set the number of requests that a single IP address can consume in the stipulated time.

c> RateLimitTimeOut

This argument takes an integer as input and is optional, the default value of this argument is set to 20. This is used to set the max time that the service or method should not be available to the client, once the rate limit exceeds.

In the OnActionExecuting method of the action filter, the base logic is implemented as this is the method that is executed before the execution of the method. In this method, we need to first fetch the IP address of the client machine, then calculate the time concerning the TateLimitTimeOut argument to get the count of requests that executed.

 

public class RateLimitAttribute : ActionFilterAttribute
{
        private readonly int RateLimit = 0;
        private readonly int RateLimitTimeOut = 0;
        private readonly string RateLimitOnMethod = string.Empty;
        private EFContext objContext = null;
        public RateLimitAttribute(string RateLimitOnMethod = "", int RateLimit = 3, int RateLimitTimeOut = 20)
        {
            this.RateLimit = RateLimit;
            this.RateLimitTimeOut = RateLimitTimeOut;
            this.RateLimitOnMethod = RateLimitOnMethod;
            if (objContext == null)
            {
                objContext = new EFContext();
            }
        }

        //
        // Summary:
        //     Called by the ASP.NET MVC framework after the action method executes.
        //
        // Parameters:
        //   filterContext:
        //     The filter context.
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            base.OnActionExecuted(filterContext);
        }
        //
        // Summary:
        //     Called by the ASP.NET MVC framework before the action method executes.
        //
        // Parameters:
        //   filterContext:
        //     The filter context.
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            //Get IP Address from HttpRequest to check if the ip exists in database 
            string IpAddress = Common.GetIpAddress(filterContext.HttpContext.Request);
            //Get the current DateTime minus the minutes set in the RateLimitTimeOut variable
            DateTime checkTime = DateTime.Now.AddMinutes(RateLimitTimeOut * -1);
            //Get number of rows in the table with respect to the IP Address and the method name set in the argument of the constructor
            //and the CreatedOn datetime should be greater than the checkTime variable.
            int Count = objContext.LoggedIPAddresses.AsQueryable().Where(x => x.IPAddress == IpAddress && x.CreatedOn > checkTime && x.MethodName == RateLimitOnMethod).OrderByDescending(x => x.CreatedOn).Take(RateLimit).Count();
            if (Count >= RateLimit)
            {
                //if count exceeds the ratelimit then we need to show error with the Http Status Code "429" which stands for "Too Many Requests".
                filterContext.Result = new HttpStatusCodeResult(429, "Too many requests");
            }
            else
            {
                //create a log in the table to verify in future requests.
                objContext.LoggedIPAddresses.Add(new LoggedIPAddress { CreatedOn = DateTime.Now, MethodName = this.RateLimitOnMethod, IPAddress = IpAddress, LogID = 0 });
                objContext.SaveChanges();
            }
            base.OnActionExecuting(filterContext);
        }
        //
        // Summary:
        //     Called by the ASP.NET MVC framework after the action result executes.
        //
        // Parameters:
        //   filterContext:
        //     The filter context.
        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            base.OnResultExecuted(filterContext);
        }
        //
        // Summary:
        //     Called by the ASP.NET MVC framework before the action result executes.
        //
        // Parameters:
        //   filterContext:
        //     The filter context.
        public override void OnResultExecuting(ResultExecutingContext filterContext)
        {
            base.OnResultExecuting(filterContext);
        }
}

 

4> Decorate the method with the attribute

Now that the Attribute is ready all we need to do is decorate the method with the attribute.

[RateLimit("UseFullResource",3, 1)]
public ActionResult UseFullResource()
{
   return Json(new { Text = "Use Full Resource" }, JsonRequestBehavior.AllowGet);
}