Building an API Gateway with Nginx + Lua

October 23, 2019

Nginx is a web server that can also be used as a reverse proxy, load balancer, mail proxy, and HTTP cache. Nginx could be used to create an API Gateway that processes requests in an event-driven manner, handling queries to the server in a quick, low-resource-footprint manner as they come in. Further, it reduces complexity and maximizes the performance by reducing the average response time to serve an API call.

Most of us are already familiar with Kong but, I wanted to explore the possibility of using OpenResty to build an API Gateway.

Implementation

For a personal website that's primarily content-focused, Astro was the perfect choice. It allowed me to deliver a fast experience without sacrificing the developer experience I love.

Design Philosophy

The first thing we need to do is to install OpenResty. OpenResty gives us the capability of scripting Nginx using Lua.

Once installed, you should navigate to your browser and type http://localhost. If everything goes well, you will see the below screenshot:

OpenResty Welcome Screenshot

If you see the default Nginx welcome page, congratulations! You've successfully installed OpenResty.

Now, we need to edit the nginx.conf file. You can find the nginx.conf file in the below locations:

  • Ubuntu: /usr/local/openresty/nginx/conf
  • Mac: /usr/local/Cellar/openresty/nginx/conf (with Homebrew)

Note: You can also add the configuration in a new file and include it in nginx.conf.

Nginx Lua API Gateway
Working of Nginx(OpenResty) as an API Gateway
For a basic API gateway, there are two important operations that it should perform:
  1. Routing
  2. Authentication

Routing

To route the endpoints to their respective servers, all you need to do is:

location /api {
    proxy_pass http://backend-api;
}

Authentication

To perform authentication, I will be using JWT. To do that, we need to install the lua-resty-jwt library using OPM(OpenResty Package Manager).

opm get SkyLothar/lua-resty-jwt
Next, create a jwt-auth.lua in /usr/local/openresty/lualib/resty (Ubuntu) and copy the below code:
local jwt = require "resty.jwt"
local validators = require "resty.jwt-validators"
if ngx.var.request_method ~= "OPTIONS" and not string.match(ngx.var.uri, "login") then
    local jwtToken = ngx.var.http_Authorization
if jwtToken == nil then
    ngx.status = ngx.HTTP_UNAUTHORIZED
    ngx.header.content_type = "application/json; charset=utf-8"
    ngx.say("{"error": "Forbidden"}")
    ngx.exit(ngx.HTTP_UNAUTHORIZED)
end

local claim_spec = {
    exp = validators.is_not_expired() -- To check expiry
}

local jwt_obj = jwt:verify('secret', jwtToken, claim_spec)
if not jwt_obj["verified"] then
    ngx.status = ngx.HTTP_UNAUTHORIZED
    ngx.header.content_type = "application/json; charset=utf-8"
    ngx.say("{"error": "INVALID_JWT"}")
    ngx.exit(ngx.HTTP_UNAUTHORIZED)
    end
end

The above Lua code does the following:

  1. It forwards the request to the next line of execution if it is an OPTIONS call or a login API.
  2. Otherwise, it fetches the JWT token in the Authorization header. If the token is not present, the code returns 'FORBIDDEN'.
  3. This JWT token is then validated for authenticity and expiry date.
  4. Once the token is validated, it forwards the request to the respective service. Else, it returns 'INVALID_JWT'.
Include the above file in your nginx.conf.

Testing

To check the changes we have made, restart OpenResty using the below command

sudo service openresty restart

Once you restart, hit the API using Postman or any other REST client and validate the response.

Tip: You can generate JWT tokens using this website: http://jwtbuilder.jamiekurtz.com/

Todo

We can improve our auth script by doing the following:

  1. A mechanism to add new API endpoints that don't need a JWT token like downloads API.
  2. Currently, JWT secret token is hardcoded in the script. Maybe we could set it as an environment variable to make it dynamic.