Background
Cloudpress allow users to export content from Google Docs to various Content Management Systems. Users can use the Cloudpress application to export the content, or they can use our Google Docs add-on.
The Cloudpress Google Docs Add-on calls a Cloudpress API endpoint to export a document. The endpoint is only used by the Google Docs Add-on and should not be accessible to any other callers.
To enforce this, I developed a custom authorization handler that checks the IP address of the caller and ensures that it comes from one of the Google IP addresses. In the remainder of this blog post, I show you how you can implement something like this for your projects.
Obtaining the list of valid IP addresses
Before we can create the authorization handler, we need to get the list of IP addresses that is used by the Google Apps Script runtime. Google publishes this list in a file located at https://www.gstatic.com/ipranges/goog.txt.
If you open this file in your browser, you will see something like the following:
This file uses CIDR (Classless Inter-Domain Routing) notation to specify the ranges of IP addresses used by the Apps Script runtime. I will not go into a lot of detail on how CIDR works, but the short of it is that 8.8.4.0/24
specifies that it includes all the IP addresses in the range from 8.8.4.0
to 8.8.4.255
.
On the .NET side, you can use the IPNetwork struct to parse CIDR and subsequently check whether a given IP address is part of that network.
Loading the IP address ranges at startup
Since this list can change over time, we want to load it every time at application startup. For this purpose, I will create a hosted service in my ASP.NET Core application.
Most of the code above is boilerplate and logging. The two highlighted lines in the StartAsync
method download the file containing the IP ranges from Google, parses it, and adds the ranges to an internal variable.
I also added a ContainsIp()
method that takes an IP address as a parameter and returns a boolean value indicating whether the IP address is part of the Google IP ranges.
The hosted service is registered with the DI container by calling the AddHostedService
extension method. Later on, we also want to be able to inject the GoogleIpNetworkStorage
instance into our authorization handler. To do that, we register it as a Singleton, where the instance is retrieved from the list of hosted services.
When our ASP.NET Core application starts up, the StartAsync
method from the hosted service will be executed and the list will be loaded from Google.
Creating a custom authorization handler
For the authorization handler, I create an authorization requirement named RequiresGoogleIpAddressRequirement
and an authorization handler named RequiresGoogleIpAddressAuthorizationHandler
that will evaluate the requirement.
The authorization handler itself is fairly simple as it retrieves the IP address of the caller from the RemoteIpAddress
property and then query the GoogleIpNetworkStorage
instance that was injected to determine if the IP address is part of the Google network.
We also need to add a custom authorization policy with the RequiresGoogleIpAddressRequirement
requirment.
And ensure that we specify that requirement for the endpoints we want to protect.
Testing the authorization handler locally
If we run the application and make a call to our weather endpoint, we’ll get a 401
(Unauthorized) response.
This is the correct behaviour, as the remote IP Address is our local IP (127.0.0.1
) which is not one of the Google IP addresses. We need a way to “spoof” one of the Google IP addresses so we can confirm our authorization handler is working as intended.
For this we can configure the ForwardedHeadersOptions
to allow the X-Forwarded-For
header.
And then add the forwarded headers middleware to our pipeline. Make sure to add this as the first middleware in the pipeline, so the remote IP address will be set according to the X-Forwarded-For
header.
Once gain, we can test this and pass one of the valid Google IPs in the X-Forwarded-For
header. This time, the authorization handler succeeds, and we get a 200
(OK) response.
Conclusion
In this blog post, I showed how you can create an authorization handler for ASP.NET Core that restricts the caller of API endpoints to specific IP addresses. You can find the code for this blog post at https://github.com/jerriepelser-blog/limit-api-callers-to-specific-ip-addresses