Hands-On: I wrote my auth logic with JS in Nginx; Will you?

Hands-On: I wrote my auth logic with JS in Nginx; Will you?

This may come as a surprise to you. How can someone use nginx configuration with Javascript? Fortunately, nginx supports a language called njs which is a strict subset of ECMA5. This can be used to further extend your routing logic, but don't be that happy because the feature you get with njs is very limited.

Lots of syntax doesn't work out of the box and I couldn't make it work with third-party npm modules such as jsonwebtoken etc. I couldn't even use esbuild to generate a single file build.

From the documentation of nginx:

njs is a subset of the JavaScript language that allows extending nginx functionality. njs is created in compliance with ECMAScript 5.1 (strict mode) with some ECMAScript 6 and later extensions. The compliance is still evolving.

I won't go with the installation process because you can get it easily in the documentation here. Also, you'd have to add the following repositories to have nginx packages available if you're also using Ubuntu.

# NGINX Repository
deb http://nginx.org/packages/mainline/ubuntu/ jammy nginx
deb-src http://nginx.org/packages/mainline/ubuntu/ jammy nginx

Running Hello World

After, we've installed the njs module. Let's go ahead and write the js code for straight-up firing the hello world.


@machine:/etc/nginx sudo cat > index.js << EOF
> var hello = function (r) {
    r.return(200, "Hello world!");
};
export default { hello: hello };
> EOF
root@machine:/etc/nginx

This is a very simple handler, which will return the string "Hello world!" with status code 200.

Let's plug it into the nginx.conf file.

user  nginx;
worker_processes  auto;

load_module modules/ngx_http_js_module.so;

http {
  js_import index.js;
  server {
      location / {
          js_content index.hello;
      }
  }
}

Now, when I send GET request to localhost with curl, I get the expected response.

root@machine:/etc/nginx# curl localhost
Hello world!
root@machine:/etc/nginx#

This is already very cool. But, we can do even more.

Implementing Dummy Auth and User Service

Let's create two simple services; one auth service and one for consuming the auth service.

Auth Service: Code

Auth Service

This is a fairly simple API. The login handler takes a POST request with username JSON key and generates a JWT token. The verify handler verifies the JWT token passed as a bearer token.

Let's test our auth service if it's working as intended.

regmicmahesh@machine:/etc/nginx$ curl -s localhost:8080/login -XPOST -d '{"username":"mahesh"}'
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTUyMjUzMjMsInVzZXJuYW1lIjoibWFoZXNoIn0.y7kOx9dScMwg0XjRj1AAa9xh3rZp0GzEzR0FC1f3x-w"}
regmicmahesh@machine:/etc/nginx$ curl -s localhost:8080/login -XPOST -d '{"username":"mahesh"}' | jq -r ".token"
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTUyMjUzMjYsInVzZXJuYW1lIjoibWFoZXNoIn0.XXkLCwspeYYy8kCONSSPve1T7AYX94GgzNB1rrdOygk
regmicmahesh@machine:/etc/nginx$ k^C
regmicmahesh@machine:/etc/nginx$ curl localhost:8080/verify -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTUyMjI5NTksInVzZXJuYW1lIjoibWFoZXNoIn0.GXnnmLuXRr8tu1Sx08DXcgs7N6au4huku-S62sR0JCU'
{"exp":1695222959,"username":"mahesh"}

User Service: Code

The user-service simply prints out the value of the header X-User-Id .

Now, we need to configure our nginx such that, every request first goes through the auth service, decodes the token if possible, and passes the username to user-service

Writing Nginx Config

First of all, let's configure the /verify .

 location = /verify {
   internal;
   proxy_pass http://localhost:8080/verify;
}

This is okay enough. The auth-service is running on port 8080. This should pass the request to that endpoint whenever requested.

Now, we can simply use auth_request /verify . But, where's the fun in that? We want to set the username in the header dynamically.

Let's write a simple njs script to send a request to this endpoint.

function verify(originalR) {
  function callback(r) {
    originalR.return(r.status);
  }

  originalR.subrequest("/verify", { method: "GET", body: "" }, callback);
}
export default { verify };

The NJS script above simply sends the request to /verify but doesn't send the request body. It sends only the headers.

Let's add one more block in nginx to use this js function.

 location = /auth {
   internal;
   js_content index.verify;
}

Now, we need to use this /auth endpoint as the authentication endpoint and everything should be good. We just wrapped our actual verification endpoint with a proxy. Why? Because now we can add our JS logic to set the header.

Let's define a variable to hold the value of the username.

server {
    set $username "";
# other code
}

Now, we can set this variable from the NJS script.

function verify(originalR) {
  function callback(r) {
    if (r.status === 200) {
      const body = JSON.parse(r.responseText);
      originalR.variables.username = body.username; 
      originalR.return(200);
    } else {
      originalR.return(401);
    }
  }

  originalR.subrequest("/verify", { method: "GET", body: "" }, callback);
}

export default { verify };

Now, we can send this variable as a header to the actual user service.

Adding it in the location, block it comes as follows:

location / {
    auth_request /auth;
    proxy_set_header "X-User-Id" $username;
    proxy_pass http://localhost:8081;
}

Let's take it for a test-run. It works!

regmicmahesh@machine:/etc/nginx$ curl  localhost -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTUyMjI5NTksInVzZXJuYW1lIjoibWFoZXNoIn0.GXnnmLuXRr8tu1Sx08DXcgs7N6au4huku-S62sR0JCU"
Request Received From: mahesh
regmicmahesh@machine:/etc/nginx$

Thanks for reading my blog. I don't think this is the best way, but just wanted to share what's possible with NJS scripting.