IP Filtering in ASP.NET Web API

Β· 1103 words Β· 6 minutes to read

One of the functionalities I had to use fairly often on different ASP.NET Web API projects that I was involved in in the past was IP filtering - restricting access to the whole API, or to parts of it, based on the caller’s IP address.

I thought it might be useful to share this here. More after the jump.

Configuration πŸ”—

Whenever you build a functionality like that, there are two roads you might wanna take:

  • as a whitelist - meaning deny the majority of the callers, and only let some predefined ones through
  • as a blacklist - meaning allow the majority of the callers, and only block some predefines ones

While you could approach the task from many different angles, for the purpose of this post, let’s assume you want to have it configurable from the web.config file, and that you will use a whitelist approach (reject everyone, unless they are in the config).

Configuration in ASP.NET is far from the friendliest (at least until we can get ASP.NET Core), and there is some ugly configuration code we will have to write - to deal with configuration elements, sections and so on. We will be extending the types from System.Configuration to provide a reasonably friendly user experience.

We could probably achieve the same with just appSettings but having a dedicated configuration section would be a bit more elegant for the end user.

So let’s imagine we will want to have configuration like this:

  <configSections>

    <section name="ipFiltering" type="Strathweb.IpFiltering.Configuration.IpFilteringSection, Strathweb.IpFiltering" />

  </configSections>

  <ipFiltering> 

    <ipAddresses>

      <add address="192.168.0.11" />

    </ipAddresses>

  </ipFiltering>

To achieve this, here are our nasty ASP.NET configuration components. First the section:

public class IpFilteringSection : ConfigurationSection

{

    [ConfigurationProperty("ipAddresses", IsDefaultCollection = true)]

    public IpAddressElementCollection IpAddresses

    {

        get { return (IpAddressElementCollection)this["ipAddresses"]; }

        set { this["ipAddresses"] = value; }

    }

}

Next, the addresses collections:

[ConfigurationCollection(typeof(IpAddressElement))]

public class IpAddressElementCollection : ConfigurationElementCollection

{

    protected override ConfigurationElement CreateNewElement()

    {

        return new IpAddressElement();

    }



    protected override object GetElementKey(ConfigurationElement element)

    {

        return ((IpAddressElement)element).Address;

    }

}

And finally the individual entries:

public class IpAddressElement : ConfigurationElement

{

    [ConfigurationProperty("address", IsKey = true, IsRequired = true)]

    public string Address

    {

        get { return (string)this["address"]; }

        set { this["address"] = value; }

    }



    [ConfigurationProperty("denied", IsRequired = false)]

    public bool Denied

    {

        get { return (bool)this["denied"]; }

        set { this["denied"] = value; }

    }

}

Implementation of IP filtering πŸ”—

Once we got the configuration out of the way, the usage can take four forms:

  • a message handler (wrapping your entire Web API)
  • a filter (applied on a specific action only)
  • an HttpRequestMessage extension methods (so that it can be called anywhere, i.e. inside a controller)
  • and, as a bonus, an OWIN middleware (wrapping your entire OWIN pipeline, if you are using it)

We should actually start with the extension method, as it’s going to be the base for everything - in the other components (except for OWIN middleware), we will just call that.

The extension method is shown below:

    public static bool IsIpAllowed(this HttpRequestMessage request)

    {

        if (!request.GetRequestContext().IsLocal)

        {

            var ipAddress = request.GetClientIpAddress();

            var ipFiltering = ConfigurationManager.GetSection("ipFiltering") as IpFilteringSection;

            if (ipFiltering != null && ipFiltering.IpAddresses != null && ipFiltering.IpAddresses.Count > 0)

            {

                if (ipFiltering.IpAddresses.Cast<IpAddressElement>().Any(ip => (ipAddress == ip.Address && !ip.Denied)))

                {

                    return true;

                }



                return false;

            }

        }



        return true;

So in the extension method, we check if the request is local, and if it isn’t we will proceed to grabbing the IP address from the request.

Then we consult our configuration and if the caller’s address is found in our configuration and check whether the IP should be allowed or not. This check could be done in a more elaborate way (see for example here) - but for our use case it’s enough to just compare them in a simple way, without working about stuff like IP ranges.

We made use of another extension method in the above snippet - (request.GetClientIpAddress()) - I blogged about it a while back and it has also been added to WebApiContrib but for the record, here it is:

public static class HttpRequestMessageExtensions

{

    private const string HttpContext = "MS_HttpContext";

    private const string RemoteEndpointMessage = "System.ServiceModel.Channels.RemoteEndpointMessageProperty";

    private const string OwinContext = "MS_OwinContext";



    public static string GetClientIpAddress(this HttpRequestMessage request)

    {

        //Web-hosting

        if (request.Properties.ContainsKey(HttpContext))

        {

            dynamic ctx = request.Properties[HttpContext];

            if (ctx != null)

            {

                return ctx.Request.UserHostAddress;

            }

        }

        //Self-hosting

        if (request.Properties.ContainsKey(RemoteEndpointMessage))

        {

            dynamic remoteEndpoint = request.Properties[RemoteEndpointMessage];

            if (remoteEndpoint != null)

            {

                return remoteEndpoint.Address;

            }

        }

        //Owin-hosting

        if (request.Properties.ContainsKey(OwinContext))

        {

            dynamic ctx = request.Properties[OwinContext];

            if (ctx != null)

            {

                return ctx.Request.RemoteIpAddress;

            }

        }

        return null;

    }

}

So now we can proceed towards building our individual components, as they will all rely on the extension method that we created.

First a filter:

public class IpFilterAttribute : AuthorizeAttribute

{

    protected override bool IsAuthorized(HttpActionContext actionContext)

    {

        return actionContext.Request.IsIpAllowed();

    }

}

And now a handler:

public class IpFilterHandler : DelegatingHandler

{

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,

        CancellationToken cancellationToken)

    {

        if (request.IsIpAllowed())

        {

            return await base.SendAsync(request, cancellationToken);

        }



        return request.CreateErrorResponse(HttpStatusCode.Forbidden, "Cannot view this resource");

    }

}

We could make an OWIN middleware too. For this we will need a new extension method - working directly off the OWIN dictionary and grabbing the IP information from there.

For Project Katana purposes, we can make the middleware Microsoft specific, and as such we could build the extension method off IOwinContext instead of the raw OWIN dictionary (since Web API on OWIN relies on Katana anyway).

That code is shown below, and is quite similar to the HttpRequestMessage extension code we wrote a bit earlier.

public static class OwinContextExtensions

{

    public static bool IsIpAllowed(this IOwinContext ctx)

    {

        var ipAddress = ctx.Request.RemoteIpAddress;

        var ipFiltering = ConfigurationManager.GetSection("ipFiltering") as IpFilteringSection;

        if (ipFiltering != null && ipFiltering.IpAddresses != null && ipFiltering.IpAddresses.Count > 0)

        {

            if (ipFiltering.IpAddresses.Cast<IpAddressElement>().Any(ip => (ipAddress == ip.Address && !ip.Denied)))

            {

                return true;

            }



            return false;

        }



        return true;

    }

}

And finally, let’s add the middleware itself (again - Katana specific middleware to be precise):

public class IpFilterMiddleware : OwinMiddleware

{

    public IpFilterMiddleware(OwinMiddleware next) : base(next)

    {

    }



    public override async Task Invoke(IOwinContext context)

    {

        if (context.IsIpAllowed())

        {

            await Next.Invoke(context);

        } 

        else 

        {

            context.Response.StatusCode = 403;

        }

    }

}

And that’s it! You can now use the components on:

  • individual actions or controller (the filter)
  • on the whole API (the message handler)
  • on the whole OWIN pipeline (the middleware)
  • or whereever you see fit (the Request extension method)

Of course you can make it much better and more robust, add support for submasks and such, add extra dynamic configuration, instead of static IP look up from web.config and so on - but hopefully this will be a good starting point.

All the source code for this article is at Github.

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP