Using NGINX Plus and NGINX to Authenticate Application Users with LDAP

Customers frequently ask us how they can use NGINX Plus and NGINX to secure protected resources or applications by authenticating the users who request them. Today we’re announcing a reference implementation of such an authentication system, and making it available in the NGINX, Inc. repository on GitHub. In this post we describe how the implementation works, how to install it, and how to use it as a model for your own authentication system.

The solution takes advantage of the ngx_http_auth_request_module module in NGINX Plus and NGINX, which forwards authentication requests to an external service. In the reference implementation, that service is a daemon we call ldap‑auth. It’s written in Python and communicates with a Lightweight Directory Access Protocol (LDAP) authentication server  – OpenLDAP by default, but we have tested the ldap‑auth daemon against default configurations of Microsoft® Windows® Server Active Directory as well (both the 2003 and 2012 versions).

The ldap‑auth daemon serves as a model for your own “connector” app, which you might write in other languages, deploy with different authentication systems, or both. The NGINX Professional Services team is available to assist with such adaptations.

Note: For ease of reading, the rest of this article refers to NGINX Plus, but the reference implementation also works with the open source NGINX software. The prerequisite http_auth_request module is included in both NGINX Plus packages and prebuilt NGINX binaries.

How Authentication Works in the Reference Implementation

To perform authentication, the http_auth_request module makes an HTTP subrequest to the ldap‑auth daemon, which acts as intermediary and interprets the subrequest for the LDAP server – it uses HTTP for communication with NGINX Plus and the appropriate API for communication with the LDAP server.

We assume that if you’re interested in the reference implementation, you already have an application or other resources you want to protect by requiring authentication. To make it easier to test the reference implementation, however, we’re providing a sample backend daemon, also written in Python, which listens on port 9000. It can stand in for an actual HTTP application during testing, by prompting for user credentials and creating a cookie based on them.

The NGINX Plus reference implementation for LDAP authentication includes the ldap-auth daemon and a sample backend daemon

Here’s a step‑by‑step description of the authentication process in the reference implementation. The details are determined by settings in the nginx-ldap-auth.conf configuration file; see Configuring the Reference Implementation below. The flow chart following the steps summarizes the process.

  1. A client sends an HTTP request for a protected resource hosted on a server for which NGINX Plus is acting as reverse proxy.

  2. NGINX Plus (specifically, the http_auth_request module) forwards the request to the ldap‑auth daemon, which responds with HTTP code 401 because no credentials were provided.

  3. NGINX Plus forwards the request to http://backend/login, which corresponds to the backend daemon. It writes the original request URI to the X-Target header of the forwarded request.

  4. The backend daemon sends the client a login form (the form is defined in the Python code for the daemon). As configured by the error_page directive, NGINX sets the HTTP code on the login form to 200.

  5. The user fills in the Username and Password fields on the form and clicks the Login button. Per the code in the form, the client generates an HTTP POST request directed to /login, which NGINX Plus forwards to the backend daemon.

  6. The backend daemon constructs a string of the format username:password, applies Base64 encoding, generates a cookie called nginxauth with its value set to the encoded string, and sends the cookie to the client. It sets the httponly flag to prevent use of JavaScript to read or manipulate the cookie (protecting against the cross‑site scripting [XSS] vulnerability).

  7. The client retransmits its original request (from Step 1), this time including the cookie in the Cookie field of the HTTP header. NGINX Plus forwards the request to the ldap‑auth daemon (as in Step 2).

  8. The ldap‑auth daemon decodes the cookie, and sends the username and password to the LDAP server in an authentication request.

  9. The next action depends on whether the LDAP server successfully authenticates the user:

    • If authentication succeeds, the ldap‑auth daemon sends HTTP code 200 to NGINX Plus. NGINX Plus requests the resource from the backend daemon. In the reference implementation, the backend daemon returns the following text:

         Hello, world! Requested URL: URL

      The nginx-ldap-auth.conf file includes directives for caching the results of the authentication attempt; to disable caching, see Caching below.

    • If authentication fails, the ldap‑auth daemon sends HTTP code 403 to NGINX Plus. NGINX Plus forwards the request to the backend daemon again (as in Step 3), and the process repeats.

In the NGINX Plus reference implementation for LDAP authentication, the ldap-auth daemon is the intermediary between NGINX Plus and the LDAP server

Installing the Components

The NGINX Plus configuration file distributed with the reference implementation, nginx-ldap-auth.conf, configures all components other than the LDAP server (that is, NGINX Plus, the client, the ldap‑auth daemon, and the backend daemon) to run on the same host, which is adequate for testing purposes. The LDAP server can also run on that host during testing.

In an actual deployment, the backend application and authentication server typically each run on a separate host, with NGINX Plus on a third host. The ldap-auth daemon does not consume many resources in most situations, so it can run on the NGINX Plus host or another host of your choice.

  1. Create a clone of the GitHub repository.

  2. If NGINX Plus is not already running, install it according to the instructions for your operating system.

  3. If an LDAP server is not already running, install and configure one. By default the ldap‑auth daemon communicates with OpenLDAP, but Microsoft Windows Active Directory 2003 and 2012 are also supported.

    If you are using the LDAP server only to test the reference implementation, you can use the OpenLDAP server Docker image that is available on GitHub, or you can set up a server in a virtual environment using instructions such as How To Install and Configure a Basic LDAP Server on an Ubuntu 12.04 VPS.

    Make note of the values you set for the Base DN, Bind DN, and Bind password. You will put them in the NGINX configuration file in Configuring the Reference Implementation.

  4. On the host where the ldap‑auth daemon is to run, install the following additional software. We recommend using the versions that are distributed with the operating system, instead of downloading the software from an open source repository.

    • Python version 2. Version 3 is not supported.
    • The Python LDAP module, python‑ldap (created by the python-ldap.org open source project).
  5. Copy the following files from your repository clone to the indicated hosts:

    • nginx-ldap-auth.conf – NGINX Plus configuration file that includes the minimal set of directives for testing the reference implementation. Install on the NGINX Plus host (in the /etc/nginx/conf.d directory if using the conventional configuration scheme). To avoid configuration conflicts, remember to move or rename any default configuration files installed with NGINX Plus.

    • nginx-ldap-auth-daemon.py – Python code for the ldap‑auth daemon. Install on the host of your choice.

    • nginx-ldap-auth-daemon-ctl.sh – Sample shell script for starting and stopping the daemon. Install on the same host as the ldap‑auth daemon.

    • backend-sample-app.py – Python code for the daemon that during testing stands in for a backend application server. Install on the host of your choice.

  6. Modify the NGINX Plus configuration file as described in Configuring the Reference Implementation below. After making your changes, run the nginx -t command to verify that the file is syntactically valid.

    root# nginx -t
    nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
    nginx: configuration file /etc/nginx/nginx.conf test is successful
  7. Start NGINX Plus. If NGINX Plus is already running, run the following command to reload the configuration file:

    root# nginx -s reload
  8. Run the following commands on the appropriate hosts to start the ldap‑auth daemon and the backend daemon.

    root# nginx-ldap-auth-daemon-ctl.sh start
    root# python backend-sample-app.py
  9. Use a web browser to access http://nginx-server-address:8081. Verify that the browser presents the authentication form. After you fill out the form and submit it, verify that the server returns the expected response to valid credentials. As noted above, the backend daemon returns the following text:

       Hello, world! Requested URL: URL

Configuring the Reference Implementation

Make the following changes in the nginx-ldap-auth.conf file. Some are required and some optional, as indicated.

LDAP Server Settings

As implemented in nginx-ldap-auth-daemon.py, the ldap‑auth daemon communicates with an OpenLDAP server, passing in parameters to specify which user account to authenticate. To eliminate the need to modify the Python code, the nginx-ldap-auth.conf file contains proxy_set_header directives that set values in the HTTP header that are then used to set the parameters. The following table maps the parameters and headers.

LDAP Parameter HTTP Header
basedn X-Ldap-BaseDN
binddn X-Ldap-BindDN
bindpasswd X-Ldap-BindPass
cookiename X-CookieName
realm X-Ldap-Realm
template X-Ldap-Template
url X-Ldap-URL
  • (Required) In the following directives, replace the values in bold with the correct values for your LDAP server deployment. Note in particular that the nginx-ldap-auth.conf file uses the well‑known port for LDAPS, 636. If you change the port to 389 (the well‑known port for LDAP) or another LDAP port, remember also to change the protocol name from ldaps to ldap.

    # URL and port for connecting to the LDAP server
    proxy_set_header X-Ldap-URL "ldaps://example.com:636";

    # Base DN
    proxy_set_header X-Ldap-BaseDN "cn=Users,dc=test,dc=local";

    # Bind DN
    proxy_set_header X-Ldap-BindDN "cn=root,dc=test,dc=local";

    # Bind password
    proxy_set_header X-Ldap-BindPass "secret";

  • (Required if using Active Directory instead of OpenLDAP) Uncomment the following directive as shown:

    proxy_set_header X-Ldap-Template "(SAMAccountName=%(username)s)";
  • (Optional) The reference implementation uses cookie‑based authentication. If you are using HTTP basic authentication instead, comment out the following directives as shown:

    #proxy_set_header X-CookieName "nginxauth";
    #proxy_set_header Cookie nginxauth=$cookie_nginxauth;
  • (Optional) If you want to change the value for the template parameter that the ldap‑auth daemon passes to the OpenLDAP server by default, uncomment the following directive as shown, and change the value:

    proxy_set_header X-Ldap-Template "(cn=%(username)s)";
  • (Optional) If you want to change the realm name from the default value (Restricted), uncomment and change the following directive:

    proxy_set_header X-Ldap-Realm "Restricted";

IP Address for Backend Daemon

If the backend daemon is not running on the same host as NGINX Plus, change the IP address for it in the upstream configuration block:

upstream backend {
server 127.0.0.1:9000;
}

IP Address for ldap‑auth Daemon

If the ldap‑auth daemon is not running on the same host as NGINX Plus, change the IP address in this proxy_pass directive:

location = /auth-proxy {
proxy_pass http://127.0.0.1:8888;
# ...
}

IP Address and Port on Which NGINX Listens

If the client is not running on the same host as NGINX Plus, change the IP address in this listen directive (or remove the address completely to accept traffic from any client). You can also change the port on which NGINX listens from 8081 if you wish:

server {
listen 127.0.0.1:8081;
# ...
}

Caching

The nginx-ldap-auth.conf file enables caching of both data and credentials. Optionally, you can change the following settings:

  • The proxy_cache_path directive in the http configuration block creates a local disk directory called cache, and allocates 10 MB in shared memory for a zone called auth_cache, where metadata is stored.

    proxy_cache_path cache/ keys_zone=auth_cache:10m;

    If you change the name of the shared memory zone, you must also change it in the proxy_cache directive (in the location block that directs traffic to the ldap‑auth daemon).

    location = /auth-proxy {
    proxy_cache auth_cache;
    # ...
    }
  • The proxy_cache_valid directive (in the same location block as proxy_cache) specifies that cached responses marked with HTTP code 200 or 403 are valid for 10 minutes.

    location = /auth-proxy {
    proxy_cache_valid 200 403 10m;
    # ...
    }

To disable caching, comment out these three directives plus the proxy_cache_key directive.

Customizing the Authentication System

As mentioned above, you can use the ldap‑auth daemon as a model for your own application that accepts requests from the http_auth_request module. If writing an app in Python to communicate with a different (non‑LDAP) type of authentication server, write a new authentication‑handler class to replace LDAPAuthHandler in the nginx-ldap-auth-daemon.py script.

A Note about Security

The backend daemon uses Base64 encoding on the username and password in the cookie. Base64 is a very weak form of scrambling, rendering the credentials vulnerable to extraction and misuse. For authentication to serve any real purpose, you need to use more sophisticated encryption in your backend application.

Cover image
Microservices: From Design to Deployment
The complete guide to microservices development