Exposing Your API Should be Easy
How I exposed an API running locally in a secure way, using ngrok and Zuplo
Every week we curate content coming from dozens of sources. One of the challenges we have is related to article categorization and summarization. To help with those tasks I've been putting together an API. While initially I was the only one using the API, I'm now exploring the possibility of exposing it publicly.
This article is brought to you with the help of our supporters: Zuplo and Speakeasy.
Zuplo is the only API Gateway that runs your OpenAPI definition directly. If you care about high-quality developer documentation, API analytics, and zero configuration deployments, give Zuplo a try today.
Speakeasy provides you with the tools to craft truly developer-friendly integration experiences for your APIs: idiomatic, strongly typed, lightweight & customizable SDKs in 8+ languages, Terraform providers & always-in-sync docs. Increase API user adoption with friction-free integrations. Try Speakeasy now.
First I started by running the API locally on my own laptop. It was good enough for what I needed. The API accepts a URL as the input, processes it, and returns an abridged version of its content. Working locally has many advantages. You don't have to worry about security, authentication, or access control. You also don't need to care about keeping the API running all the time or even making sure other consumers can access it.
After some time I built a bookmarklet that uses the API and shows a UI with the abridged content. That was the time when I had to make the API available inside my LAN. At that point, any consumer inside my local network could access the API freely. Access was not restricted at the API layer because only local network consumers could reach it. Even though I have been playing with the idea of exposing the API publicly, I couldn't justify doing it.
That was until recently. If we're finding value in using the API why shouldn't we expose it publicly so we can easily show other people what it can do? So, I started planning how I could make the API available to anyone on the Internet. The first step would be to deploy the API somewhere. However, I didn't want to incur any unnecessary costs so I thought of exposing remotely the locally running API. To do that I used ngrok and exposed my local development port via HTTPS. Suddenly the API was available from anywhere on its own URL.
Exposing a local port with ngrok is quite simple. After creating an account you go to the "Setup & Installation" screen and follow the instructions for your platform. In my case, I decided to install an agent on my laptop running macOS. I could have opted for an SDK—which I'll probably do at a later stage—but I wanted to start with something simple. After having the agent installed I followed the instructions for deploying my app on a static domain and I was done. Exciting!
This was great but now anyone could access the API without any restrictions. And I didn't want that to happen. So, I thought of using a gateway to add authentication to the API. I created an account on Zuplo because I knew I could use my existing OpenAPI definition out of the box. Then I created a project to start defining how the gateway would handle my API.
After having the Zuplo project created, I immediately imported my OpenAPI definition. I could suddenly view the definition of my API inside Zuplo's Route Designer. I opened one of the operations and could see its default configuration. It was forwarding the request to localhost
, which made sense because that's what I have on the OpenAPI definition. So, I changed it to forward the request to the URL I got from ngrok. I tested accessing the URL provided by Zuplo and it worked! Zuplo was already forwarding the request to ngrok, which was tunneling it to my laptop.
However, now I added another layer of complexity but didn't fix the problem of unauthorized access I had in the first place. To do that, I added the api-key-inbound
policy to the request. Now, every time someone tries to access the API via Zuplo will need to authenticate using an API key. To create an API key you go to Zuplo's Project Settings and open the API Key Consumers option. You can then create a new consumer and obtain the generated API key. With the API key, you can now make an authenticated request to the API by adding the Authorization
header with the value Bearer
followed by the key. All this looks good but I can still bypass the authentication by making a request directly to the ngrok URL. There must be a way to secure the connection between the gateway and my API running locally.
API gateways usually work by forwarding requests to backend services through secure connections. There are different ways to secure those connections. One way would be to have the gateway in the same network as the API or to have the gateway connect to the API through a VPN. Since these options are out of my reach I went for something simpler. If I could indicate to the API that the requests coming from the gateway are considered secure then I'd have my problem solved. I decided to use a custom HTTP header with a secret token. Every request starting on the gateway will have this header which I can validate on the API running locally. I used the set-headers-inbound
policy and configured it to add a header called gateway-auth
.
Then, on my API code, I added a validation to check if requests were arriving with that header and with the correct value. To prevent leaking the secret token on my code, I stored it locally on an environment variable called GATEWAY_AUTH
.
if (!req.headers['gateway-auth'] ||
req.headers['gateway-auth'] !=
process.env['GATEWAY_AUTH']) {
return next(createError(403, {
details: `Error fetching ${req.originalUrl}`,
instance: req.originalUrl
}));
}
So, now I have the whole authenticated flow working, starting from a request to the gateway URL and ending on my local API. The only problem is that now, even requests coming from my own laptop or local network also need to have the gateway-auth
header. I obviously want local requests to bypass this authentication verification. To do that I added code that verifies if the connection is local and, in that situation completely bypasses the gateway-auth
validation.
I now have what I was looking for and did it easily. Using the combination of ngrok and Zuplo really worked well. Zuplo's ability to read my OpenAPI definition made the initial configuration a breeze. The ability to turn on API key authentication and to secure the connection with the local API are really helpful features. I now have an API running locally which I can expose publicly in a secure and authenticated way.