Protecting web sites with NGINX subrequest authentication

30 June, 20204 min readWeb Development

TL;DR

Protecting a web site with NGINX by using authentication server via a subrequest.

  • Use auth_request /auth in NGINX conf.
  • When user requests protected area, NGINX make internal request to /auth. If 201 is returned, protected contents are served. Anything else, NGINX responds with 401.
  • /auth is reverse proxied to Express app auth-server which handles authentication. Cookies as passed on as well, so the auth server can check for a JWT.
  • Auth server sets httpOnly cookie containing a JWT.
  • JWT updated with new expiry each time a user visits protected area.

Details

For this server block, we want to protect the entire site, except the authentication areas. We can use a NGINX conf file such as like this:

location / {
    auth_request /auth;
    auth_request_set $auth_cookie $upstream_http_set_cookie;
    add_header Set-Cookie $auth_cookie;
    try_files $uri $uri/ /index.html;
}

location = /auth {
    internal;
    proxy_pass http://localhost:3003;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header X-Original-URI $request_uri;
    proxy_set_header X-Original-Remote-Addr $remote_addr;
    proxy_set_header X-Original-Host $host;
}

location ~ ^/(login|logged-in|logout)$ {
    proxy_pass http://localhost:3003;
}

Using directive auth_request /auth

We are protecting /. For each request to /* except for regex pattern ^/(auth|login|logged-in|logout)$, NGINX will send a GET request to /auth and listen to the response. This is done with the auth_request directive.

A 201 response from /auth is a successful authentication and the /* contents will be served as normal. Any other reponse from /auth is a failed authentication and the client will be served a 401 (unauthorised) response. We'll customise this 401 response later by serving a login interface.

From NGINX's documentation:

"NGINX and NGINX Plus can authenticate each request to your website with an external server or service. To perform authentication, NGINX makes an HTTP subrequest to an external server where the subrequest is verified. If the subrequest returns a 2xx response code, the access is allowed, if it returns 401 or 403, the access is denied. Such type of authentication allows implementing various authentication schemes, such as multifactor authentication, or allows implementing LDAP or OAuth authentication."

We use add_header Set-Cookie $auth_cookie so that any Set-Cookie header returned from the upstream auth server is forwarded back to the client. The auth server usually uses Set-Cookie to renew the JWT each time, so that any timeout is respected and calculated from the time of last access.

The headers from client-to-server is passed on to /auth as well, including any cookies. This is important, as a JWT is used to determine if the client is authenticated.

Send subrequests to Node-Express app auth-server

We run a Node-Express auth-server on http://localhost:3000. Then proxy all requests to /auth to app. This app will ignore any request body content when made to /auth, so we can use:

location = /auth {
    internal;
    proxy_pass http://localhost:3003;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header X-Original-URI $request_uri;
    proxy_set_header X-Original-Remote-Addr $remote_addr;
    proxy_set_header X-Original-Host $host;
}

The last 3 directives here, add an extra 3 headers to the subrequest. The auth-server could use it to determine authentication status, but it doesn't at the moment.

proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Original-Remote-Addr $remote_addr;
proxy_set_header X-Original-Host $host;

auth-server

We are running the open source auth-server (written by myself). In summary, it listens on port 3003 for the following requests:

  • GET /auth - requested by NGINX for subrequest authentication
  • POST /login - XHR made from /login interface
  • GET /login - login interface
  • GET /logged-in - tells user they are logged in, with link back to protected area and will have a logout link
  • GET /logout - forces immediate logout
  • POST /logout - XHR made from /logged-in interface

The following location block, will pass requests to those URIs to the auth-server at http://localhost:3003 with a reverse proxy.

location ~ ^/(login|logged-in|logout)$ {
    proxy_pass http://localhost:3003;
}

NGINX default error response

401 (unauthorised) errors are handled by rendering to the user the /login page. We add this to the server block

error_page 401 /login;

When a user is not authenticated and attempts to visit a protected area, it serves the /login interface.

Logging out

To log out, the client need to remove its cookie. Since it's a httpOnly cookie, the request to clear the cookies must come from a Set-Cookie response header with empty contents.

User authentication will also automatically time out from cookie expiry and JWT expiry time.

To do this, we proxy_pass a GET /logout request to the auth server, which then returns the desired Set-Cookie header which will subsequently remove the token.

© Andy Gock 2009−2020