Advanced IP address filtering in MVC and WebApi

Restrict or allow specific IP addresses to access your WebApi or MVC

  • Share

Last year I wrote an article about IP address filtering on MVC and WebApi Restrict Access to an MVC Action or Controller based on IP address. Over time the requirements for IP filtering in several web based application increased, so I had t work on this class to make it more extendible and reusable in different scenarios.

The new scenarios I wanted to cover are:

  • Allowing list of IP addresses to access the controller or controller action
  • Restricting list of IP addresses to access the controller or controller action
  • Allowing list of IP address ranges to access the controller or controller action
  • Restricting list of IP address ranges to access the controller or controller action

The class which I mentioned above, for sure did not cover these four scenarios, so I had to come up with the way to maintain basic functionality, extend with the new functionality and make the class reusable again in code.

For this I had to make some additional classes. Since there is no .NET class for IP address range out of the box, I decided too create a simple POCO class which will represent the IP address range.

    public class IPAddressRange
    {
        private IPAddress startIPAddress;
        private IPAddress endIPAddress;

        public IPAddress StartIPAddress
        {
            get
            {
                return this.startIPAddress;
            }
        }

        public IPAddress EndIPAddress
        {
            get
            {
                return this.endIPAddress;
            }
        }

        public IPAddressRange(string startIPAddress, string endIPAddress)
            : this(IPAddress.Parse(startIPAddress), IPAddress.Parse(endIPAddress))
        {

        }
        public IPAddressRange(IPAddress startIPAddress, IPAddress endIPAddress)
        {
            this.startIPAddress = startIPAddress;
            this.endIPAddress = endIPAddress;
        }
    }
    

Since I wanted to cover scenarios for both allowing and restricting the IP addresses as list or IP address ranges as list and as well as creating the restriction which applies the same rule, the best way was to pass those lists in a constructor. The problem is that I would not be able to create two constructors for blocking the list and ranges and two constructor for allowing list and ranges because they would have the same signature as they should receive same type of element lists.

The resolution for this I found in using one more property for the main class which was the enumeration value

    public enum IPAddressFilteringAction
    {
        Allow,
        Restrict
    }
    

This enumeration value will allow me to apply the logic for either blocking or allowing the request IP address.

Since I wanted to add the IP range validation as well and checking an IP against the IP range is common I implemented it as an extension method to IPAddress class.

   public static class IPAddressExtensions
    {
        public static bool IsInRange(this IPAddress address, IPAddress start, IPAddress end)
        {

            AddressFamily addressFamily = start.AddressFamily;
            byte[] lowerBytes = start.GetAddressBytes();
            byte[] upperBytes = end.GetAddressBytes();

            if (address.AddressFamily != addressFamily)
            {
                return false;
            }

            byte[] addressBytes = address.GetAddressBytes();

            bool lowerBoundary = true, upperBoundary = true;

            for (int i = 0; i < lowerBytes.Length &&
                (lowerBoundary || upperBoundary); i++)
            {
                if ((lowerBoundary && addressBytes[i] < lowerBytes[i]) ||
                    (upperBoundary && addressBytes[i] > upperBytes[i]))
                {
                    return false;
                }

                lowerBoundary &= (addressBytes[i] == lowerBytes[i]);
                upperBoundary &= (addressBytes[i] == upperBytes[i]);
            }

            return true;
        }

    }
    

The main class code will be a lot shorter since we took out this method for validation.

Now to start with the main class. As mentioned before, key entry points are constructors. Note that there are no parameterless constructors as each class instance needs to know whether to apply blocking or allowing for either IP address list or IP address ranges list.

The base inherited by our attribute class is AuthorizeAttribute, so we just need to apply our log to override of IsAuthorized method.

    public class IPAddressFilterAttribute : AuthorizeAttribute
    {
        private IEnumerable<IPAddress> ipAddresses;
        private IEnumerable<IPAddressRange> ipAddressRanges;
        private IPAddressFilteringAction filteringType;
        
        public IEnumerable<IPAddress> IPAddresses
        {
            get
            {
                return this.ipAddresses;
            }
        }

        public IEnumerable<IPAddressRange> IPAddressRanges
        {
            get
            {
                return this.ipAddressRanges;
            }
        }

        public IPAddressFilterAttribute(string ipAddress, IPAddressFilteringAction filteringType)
           : this(new IPAddress[] { IPAddress.Parse(ipAddress) }, filteringType)
        {

        }

        public IPAddressFilterAttribute(IPAddress ipAddress, IPAddressFilteringAction filteringType)
            : this(new IPAddress[] { ipAddress }, filteringType)
        {

        }

        public IPAddressFilterAttribute(IEnumerable<string> ipAddresses, IPAddressFilteringAction filteringType)
            : this(ipAddresses.Select(a => IPAddress.Parse(a)), filteringType)
        {

        }

        public IPAddressFilterAttribute(IEnumerable<IPAddress> ipAddresses, IPAddressFilteringAction filteringType)
        {
            this.ipAddresses = ipAddresses;
            this.filteringType = filteringType;
        }

        public IPAddressFilterAttribute(string ipAddressRangeStart, string ipAddressRangeEnd, IPAddressFilteringAction filteringType)
            : this(new IPAddressRange[] { new IPAddressRange(ipAddressRangeStart, ipAddressRangeEnd) }, filteringType)
        {

        }

        public IPAddressFilterAttribute(IPAddressRange ipAddressRange, IPAddressFilteringAction filteringType)
            :this(new IPAddressRange[] { ipAddressRange }, filteringType)
        {

        }

        public IPAddressFilterAttribute(IEnumerable<IPAddressRange> ipAddressRanges, IPAddressFilteringAction filteringType)
        {
            this.ipAddressRanges = ipAddressRanges;
            this.filteringType = filteringType;
        }

        protected override bool IsAuthorized(HttpActionContext context)
        {
            string ipAddressString = ((HttpContextWrapper)context.Request.Properties["MS_HttpContext"]).Request.UserHostName;
            return IsIPAddressAllowed(ipAddressString);
        }

        private bool IsIPAddressAllowed(string ipAddressString)
        {
            IPAddress ipAddress = IPAddress.Parse(ipAddressString);

            if (this.filteringType == IPAddressFilteringAction.Allow)
            {
                if (this.ipAddresses != null && this.ipAddresses.Any() &&
                    !IsIPAddressInList(ipAddressString.Trim()))
                {
                    return false;
                }
                else if (this.ipAddressRanges != null && this.ipAddressRanges.Any() &&
                    !this.ipAddressRanges.Where(r => ipAddress.IsInRange(r.StartIPAddress, r.EndIPAddress)).Any())
                {
                    return false;
                }

            }
            else
            {
                if (this.ipAddresses != null && this.ipAddresses.Any() &&
                   IsIPAddressInList(ipAddressString.Trim()))
                {
                    return false;
                }
                else if (this.ipAddressRanges != null && this.ipAddressRanges.Any() &&
                    this.ipAddressRanges.Where(r => ipAddress.IsInRange(r.StartIPAddress, r.EndIPAddress)).Any())
                {
                    return false;
                }

            }

            return true;

        }

        private bool IsIPAddressInList(string ipAddress)
        {
            if (!string.IsNullOrWhiteSpace(ipAddress))
            {
                IEnumerable<string> addresses = this.ipAddresses.Select(a => a.ToString());
                return addresses.Where(a => a.Trim().Equals(ipAddress, StringComparison.InvariantCultureIgnoreCase)).Any();
            }
            return false;
        }

    }
    

This way both blocking or allowing only certain list of IPs or ranges is much more easy to control with a simple custom attribute.

Note

Complete source code can be downloaded from GitHub project https://github.com/dejanstojanovic/IP-Address-Filtering

References

  • Share

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.

Comments for this article

comments powered by Disqus