Custom Single Sign-On with Nginx and Auth Request Module

In a recent project I crashed into a wall.  At least for a couple of days that is.  The requirement was to integrate the Request Tracker (aka RT) installation on CentOS 7 server with Nginx to a client’s company single sign-on solution.  Which wasn’t LDAP.  Or Active Directory.  Or anything standard at all – a complete homegrown system.

First set of Google searches firmly suggested to use ngx_http_auth_request_module.  The instructions seemed quite straightforward.  However, I was struggling to make it work.  The reason for that was trivial – Nginx web server shipped with CentOS 7 is compiled without this module:

# rpm -qi nginx
Name        : nginx
Epoch       : 1
Version     : 1.6.3
Release     : 6.el7
Architecture: x86_64
Install Date: Wed 05 Aug 2015 06:37:41 AM UTC
Group       : System Environment/Daemons
Size        : 1441031
License     : BSD
Signature   : RSA/SHA256, Wed 08 Jul 2015 01:38:40 PM UTC, Key ID 6a2faea2352c64e5
Source RPM  : nginx-1.6.3-6.el7.src.rpm
Build Date  : Fri 03 Jul 2015 12:44:51 PM UTC
Build Host  : buildvm-09.phx2.fedoraproject.org
Relocations : (not relocatable)
Packager    : Fedora Project
Vendor      : Fedora Project
URL         : http://nginx.org/
Summary     : A high performance web server and reverse proxy server
Description :
Nginx is a web server and a reverse proxy server for HTTP, SMTP, POP3 and
IMAP protocols, with a strong focus on high concurrency, performance and low
memory usage.

I do know how to build custom RPMs, but that would mean a much increased maintenance, which I wanted to avoid.

So, the next question is: are there any compiled modules that I can use to solve the problem?

# nginx -V
nginx version: nginx/1.6.3
built by gcc 4.8.3 20140911 (Red Hat 4.8.3-9) (GCC)
TLS SNI support enabled
configure arguments: --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_spdy_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module --with-http_image_filter_module --with-http_geoip_module --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_stub_status_module --with-http_perl_module --with-mail --with-mail_ssl_module --with-pcre --with-pcre-jit --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E'

If you look carefully enough, you’ll notice the ngx_http_perl_module.  Great news!  If I have Perl, I can do anything.  A quick scan through the documentation confirms that I should be able to access the request object.

Fast forward a few hours and I still got nowhere.  The Nginx part was working enough for me to have some test variables and code snippets.  But in order to work with the request object, I had to have Nginx Perl module installed on the system.  Which is not shipped with CentOS 7 as a package.  And which was failing to compile during the CPAN installation.  There was something or other about Nginx server not being compiled with sufficient parameters.  Sorry, I forgot to log the error and I moved on since.

So, is that it?  No elegant, low maintenance solution then?  Nginx rebuild required?  I was very reluctant to believe this.  And I wasn’t in a hurry.  So I stopped and got some sleep.  And as it often happens, a midnight enlightenment occurred: look for another pre-compiled package for Nginx, don’t use the one from the CentOS project.

Is there one?  Yes, of course!  In fact, there is an official Nginx repository, which supports CentOS (among other distributions), provides a much newer version of Nginx (1.8.0 instead of 1.6.3) and is compiled with the ngx_auth_request_module!

Here is how it looks:

# rpm -qi nginx
Name        : nginx
Epoch       : 1
Version     : 1.8.0
Release     : 1.el7.ngx
Architecture: x86_64
Install Date: Thu 09 Jul 2015 02:07:42 PM EEST
Group       : System Environment/Daemons
Size        : 910765
License     : 2-clause BSD-like license
Signature   : RSA/SHA1, Tue 21 Apr 2015 07:44:08 PM EEST, Key ID abf5bd827bd9bf62
Source RPM  : nginx-1.8.0-1.el7.ngx.src.rpm
Build Date  : Tue 21 Apr 2015 06:37:00 PM EEST
Build Host  : centos7-amd64-ovl
Relocations : (not relocatable)
Vendor      : nginx inc.
URL         : http://nginx.org/
Summary     : High performance web server
Description :
nginx [engine x] is an HTTP and reverse proxy server, as well as
a mail proxy server.

# nginx -V
nginx version: nginx/1.8.0
built by gcc 4.8.2 20140120 (Red Hat 4.8.2-16) (GCC) 
built with OpenSSL 1.0.1e-fips 11 Feb 2013
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-http_ssl_module --with-http_realip_module --with-http_addition_module --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_stub_status_module --with-http_auth_request_module --with-mail --with-mail_ssl_module --with-file-aio --with-ipv6 --with-http_spdy_module --with-cc-opt='-O2 -g -pipe -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic'

Excellent!  So, now on to the configuration … what do we need to do there?

First of all, RT configuration is a straightforward FastCGI setup, as per the documentation.  Adding auth request handling and we have the following location block:

location / {
  auth_request /auth;
  auth_request_set $username $upstream_http_x_username;

  fastcgi_param  REMOTE_USER        $username;

  fastcgi_param  QUERY_STRING       $query_string;
  fastcgi_param  REQUEST_METHOD     $request_method;
  fastcgi_param  CONTENT_TYPE       $content_type;
  fastcgi_param  CONTENT_LENGTH     $content_length;

  fastcgi_param  SCRIPT_NAME        '';
  fastcgi_param  PATH_INFO          $uri;
  fastcgi_param  REQUEST_URI        $request_uri;
  fastcgi_param  DOCUMENT_URI       $document_uri;
  fastcgi_param  DOCUMENT_ROOT      $document_root;
  fastcgi_param  SERVER_PROTOCOL    $server_protocol;

  fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
  fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

  fastcgi_param  REMOTE_ADDR        $remote_addr;
  fastcgi_param  REMOTE_PORT        $remote_port;
  fastcgi_param  SERVER_ADDR        $server_addr;
  fastcgi_param  SERVER_PORT        $server_port;
  fastcgi_param  SERVER_NAME        $server_name;

  fastcgi_pass 127.0.0.1:9000;
}

So, what’s happening here?

  • Line 1 tells Nginx to authenticate every request using /auth sub-request.  That will be a very simple GET request without the body – just the headers, expecting a 200 OK response status back for anything that authenticated successfully, and any other response status for an authentication failure.
  • Line 2 tells Nginx to set $username variable to the value of the X-Username response header, coming from the auth sub-request.  This is handy for those cases where you don’t only need to know if the user is successfully authenticated, but also who he or she is.
  • Line 5 sets the REMOTE_USER to the returned $username value, so that the RT can pick it up.
  • Line 28 tells Nginx where the FastCGI handler is.

Now, in the same server block we need to define the handling of the /auth location.  Here your mileage may vary, so use the below as a starting point:

location = /auth {
    proxy_pass http://localhost:8080;   
    proxy_pass_request_body off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Origin-URI $request_uri;
    proxy_set_header Content-Length '0';
}

Whether or not you have everything on the same host is irrelevant. I’m just keeping this example here for the next time where it won’t be. The important bits here are about the conversion of POST requests to GET requests. All your /auth requests will be GET, irrelevant of the original request. So dropping the body of the request and setting Content-Length to 0 sounds like a good idea.  In fact, in my original configuration I didn’t have the reset of the Content-Length, which resulted in me pulling out hair for a few hours, trying to understand why requests from 127.0.0.1 to 127.0.0.1 timeout when executed by Nginx (hint: sub-request) and succeed when executed by me from the command line (hint: direct request).

Now, for my authentication mechanism I setup the simplest of PHP virtual hosts, sending all requests to a simple script.  Here’s the shortest version purely for illustration:

<?php 
function getUsername() { 
  // ... custom authentication 
} 
$username = getUsername(); 
if ($username) { 
  header('X-Username: ' . $username, true, 200); 
} 
else {
  header('X-Username: ', true, 403); 
} 
?>

We are almost done now. The last thing to do is to send non-authenticated users to the login page. To do so, in our RT’s virtual host configuration we add the error page handler like so:

error_page 403 = @login;
location @login {
  rewrite ^.* https://secure.example.com/login;
}

Don’t forget to start the Nginx server, FastCGI handler, and PHP FPM, if you opted for the local PHP solution like I did. Cross your fingers and try it out.

Obviously, your application should support external authentication via the REMOTE_USER. For the Request Tracker, you need to set the following your etc/RT_SiteConfig.pm:

Set($WebRemoteUserAuth, 1);
Set($WebRemoteUserContinuous, 1);
Set($WebFallbackToRTLogin, undef);
Set($WebRemoteUserGecos, 1);
Set($WebRemoteUserAutocreate, 1);

Overall, it took me a while to arrive at this configuration, and it might seem slightly complicated at first, but the more I look at it, the more I like. I think it’s elegant, flexible, and quite low maintenance. Authentication mechanism is external and not web server dependent. The script itself can be adjusted, as the company moves to more standardized solution. The Nginx package is pre-compiled and from a well trusted source. And even the login redirect is handled without any hacking around.

If you have any improvement suggestions, please feel free to let me know via comments.

12 thoughts on “Custom Single Sign-On with Nginx and Auth Request Module”


    1. Request Tracker provides the regular Logout link, which kills the current user’s session. If redirected back, the user is authenticated if his SSO session is still alive. Killing the SSO session was handled by the client. So there was nothing special that we had to do for the logout.


  1. Hey.
    Thanks for this detailed post. I stumbled upon a similar issue trying to plug nginx with oauth2-proxy which is best using this module and I’m on an old CentOS 6 (believe me, I’d have upgraded long ago if I could). It worked !

Leave a Comment