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.
Notes:
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.
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 flowchart below the steps summarizes the process.
A client sends an HTTP request for a protected resource hosted on a server for which NGINX Plus is acting as reverse proxy.
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.
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.
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
.
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.
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).
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).
The ldap‑auth daemon decodes the cookie, and sends the username and password to the LDAP server in an authentication request.
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 401
to NGINX Plus. NGINX Plus forwards the request to the backend daemon again (as in Step 3), and the process repeats.
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.
Create a clone of the GitHub repository.
If NGINX Plus is not already running, install it according to the instructions for your operating system.
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 using instructions such as How To Install and Configure OpenLDAP and phpLDAPadmin on Ubuntu 16.04.
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.
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.
Copy the following files from your repository clone to the indicated hosts:
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
Start NGINX Plus. If NGINX Plus is already running, run the following command to reload the configuration file:
root# nginx -s reload
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
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
Make the following changes in the nginx-ldap-auth.conf file. Some are required and some optional, as indicated.
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";
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;
}
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;
# ...
}
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;
# ...
}
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.
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.
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.
"This blog post may reference products that are no longer available and/or no longer supported. For the most current information about available F5 NGINX products and solutions, explore our NGINX product family. NGINX is now part of F5. All previous NGINX.com links will redirect to similar NGINX content on F5.com."