Authorizing requests based on request body content [http/authorization/request_body]

Sometimes authentication credentials are passed in the request body rather than through headers or the query string. Using njs, we have full access to the request body contents.

Step 1: Use the following commands to start your NGINX container with this lab’s files: Notice the SECRET_KEY environment variable

EXAMPLE='http/authorization/request_body'
docker run --rm --name njs_example -e SECRET_KEY="foo" -v $(pwd)/conf/$EXAMPLE.conf:/etc/nginx/nginx.conf:ro -v $(pwd)/njs/:/etc/nginx/njs/:ro -p 80:80 -d nginx

Step 2: Now let’s use curl to test our NGINX server:

curl http://localhost/secure/B
No signature

curl http://localhost/secure/B?a=1 -H Signature:A
Invalid signature: YC5iL6aKDnv7XOjknEeDL+P58iw=

curl http://localhost/secure/B?a=1 -H Signature:YC5iL6aKDnv7XOjknEeDL+P58iw=
BACKEND:/secure/B

curl http://localhost/secure/B -d "a=1" -X POST -H Signature:YC5iL6aKDnv7XOjknEeDL+P58iw=
BACKEND:/secure/B

docker stop njs_example

Code Snippets

This config uses auth_request to make a request to an “authentication server” before proxying to the upstream server. In this case, the “auth server” is an internal location that calls our njs code.

nginx.conf
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  ...

  env SECRET_KEY;

  http {
        js_path "/etc/nginx/njs/";

        js_import main from http/authorization/request_body.js;

        upstream backend {
            server 127.0.0.1:8081;
        }

        server {
            listen 80;

            location /secure/ {
                js_content main.authorize;
            }

            location @app-backend {
                proxy_pass http://backend;
            }
        }

        server {
            listen 127.0.0.1:8081;
            return 200 "BACKEND:$uri\n";
        }
  }

The njs code will look for a “Signature” header and compare its contents with a digital signature generated by the crypto library of njs. If the request method is POST, we make sure the content-type is a form and then incorporate the request body (if present) into the digital signature.

request_body.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
  function authorize(r) {
      var signature = r.headersIn.Signature;

      if (!signature) {
          r.return(401, "No signature\n");
          return;
      }

      var h = require('crypto').createHmac('sha1', process.env.SECRET_KEY);

      h.update(r.uri);

      switch (r.method) {
      case 'GET':
          var args = r.variables.args;
          h.update(args ? args : "");
          break;

      case 'POST':
          var body  = r.requestBody;
          if (r.headersIn['Content-Type'] != 'application/x-www-form-urlencoded'
              || !body.length)
          {
              r.return(401, "Unsupported method\n");
          }

          h.update(body);
          break;

      default:
          r.return(401, "Unsupported method\n");
          return;
      }

      var req_sig = h.digest("base64");

      if (req_sig != signature) {
          r.return(401, `Invalid signature: ${req_sig}\n`);
          return;
      }

      r.internalRedirect('@app-backend');
  }

  export default {authorize}