Site icon Leonid Mamchenkov

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?

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.

Exit mobile version