Carry Out Group Based HTTP Basic Authentication in Nginx

1
11216

Authentication in Nginx

This tutorial explains how to configure group based HTTP Basic authentication in Nginx using a programming language called Lua, which is supported by Nginx.

If you have ever used HTTP Basic authentication in Apache extensively and then, for some reason, migrated to Nginx, you are probably missing the group based filter that Apache has for this functionality. I was using Apache till now, but recently had to shift to Nginx for performance reasons. And I found this feature missing in Nginx.
However, Nginx supports Lua, which is a programming language. And you can do pretty much anything with the power of Lua – it can be used to modify Nginx’s behaviour in numerous ways. So I developed a simple solution for group based authentication in Lua.
Before I move on to the Nginx part, let’s first recall how group based authentication was configured in Apache:

<Location /foo>
AuthType Basic
AuthName Restricted
AuthBasicProvider file
AuthUserFile /etc/apache2/htpasswd
AuthGroupFile /etc/apache2/groups
Require group somegroup
</Location>

You are basically telling Apache to use the file /etc/apache2/htpasswd to look up user names and passwords and the group membership in /etc/apache2/groups. The htpasswd file usually consists of something like what’s shown below:

foo:$apr1$2xICNyAr$rbGelq994Y3UKPhBoRwak.
bar:$apr1$pRVQ9Z65$7XX98kYIpI/1nJik7jnSs1

And the group file consists of what follows:

somegroup1: foo
somegroup2: bar

This method can be extended to LDAP/SQL authentication as well (using the appropriate Apache modules, like mod_authnz_ldap, mod_dbd and mod_authn_dbd). I won’t go into the details of these in this article as there is sufficient documentation available online.
So let’s say we write a Lua script named authenticate.lua. This is how you configure Nginx to execute the Lua script for checking access rights to a path /restricted:

location ~ ^/restricted {
set $user_group ‘somegroup’;
access_by_lua_file ‘/etc/nginx/authenticate.lua’;
}

The Lua script to make it happen is given below:

-- basic configuration of the script

local cookie_domain = “.yourdomain.com”
local db_username = “dbuser”
local db_password = “dbpasswrod”
local db_socket = “/tmp/mysql.sock”
local db_name = “dbname”

-- end configuration

local session = require “resty.session”.open{ cookie = { domain = cookie_domain } }
local remote_password

if ngx.var.http_authorization then
local tmp = ngx.var.http_authorization
tmp = tmp:sub(tmp:find(‘ ‘)+1)
tmp = ngx.decode_base64(tmp)
remote_password = tmp:sub(tmp:find(‘:’)+1)
end

function authentication_prompt()
session.data.valid_user = false
session.data.user_group = nil
session:save()
ngx.header.www_authenticate = ‘Basic realm=”Restricted”’
ngx.exit(401)
end

function authenticate(user, password, group)
local mysql = require “resty.mysql”
local db, err, errno, sqlstate, res, ok

db = mysql:new()
if not db then
ngx.log(ngx.ERR, “Failed to create mysql object”)
ngx.exit(500)
end

db:set_timeout(2000)
ok, err, errno, sqlstate = db:connect{
path = db_socket,
database = db_name,
user = db_username,
password = db_password
}

if not ok then
ngx.log(ngx.ERR, “Unable to connect to database: “, err, “: “, errno, “ “, sqlstate)
ngx.exit(500)
end

user = ngx.quote_sql_str(user)
password = ngx.quote_sql_str(password)
local query = “select 1 from http_users where username = %s and password = SHA2(%s, 224) and (find_in_set(‘superadmin’, groups) > 0 or find_in_set(‘%s’, groups) > 0)”
query = string.format(query, user, password, group);
res, err, errno, sqlstate = db:query(query)

if res and res[1] then
session.data.valid_user = true
session.data.user_group = group
session:save()
else
authentication_prompt()
end
end

if session.present and (session.data.valid_user and session.data.user_group == ngx.var.user_group) then
return
elseif ngx.var.remote_user and remote_password then
authenticate(ngx.var.remote_user, remote_password, ngx.var.user_group)
else
authentication_prompt()
end

The group authentication script looks for users and groups in a table called http_users. Since this is a script, you can modify the way users are searched for in the database or change the database altogether! The Lua modules required to run this script are resty.mysql, resty.session, resty.string and cjson. Though the passwords are stored in the database as a SHA224 hash, the comparison of the password is done by the database itself. I did not convert the password to hash before sending it to the database, so you could review this in case you are using a remote database as you may be sending passwords in clear text over the network! I’m using a local database over a UNIX socket so it doesn’t matter.

The database structure for this is as follows:
CREATE TABLE `http_users` (
`username` varchar(15) NOT NULL DEFAULT ‘’,
`password` varchar(56) NOT NULL DEFAULT ‘’,
`groups` set(‘superadmin’,’group1’,’group2’,’group3’,’group3’) NOT NULL DEFAULT ‘’,
PRIMARY KEY (`username`),
UNIQUE KEY `password` (`password`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

DELIMITER $$
CREATE trigger http_user_ins before insert on http_users for each row
begin
SET NEW.password = SHA2(NEW.password, 224);
end;
DELIMITER $$

DELIMITER $$
CREATE trigger http_user_upd before update on http_users for each row
begin
if LENGTH(NEW.password) != 56 then
SET NEW.password = SHA2(NEW.password, 224);
end if;
end;
DELIMITER $$

The triggers are required to convert the INSERT or UPDATE statements into SHA224. I’ve used MySQL’s SET data type to ensure that the group value cannot contain errors – that is, it can only have a fixed set of values. This gets even better when you use PHPMyAdmin – it will give a list of check boxes for each of the group values. The same values can be used by Nginx in the $user_group variable before specifying the access_by_lua_file directive.
However, there is one problem with this method of securing access. This is that if you embed a location statement inside the location block in which you are calling the authentication script, as shown below (which happens to be a common pattern with modern PHP Web applications)…

location ~ ^/restricted {
set $user_group ‘somegroup’;
access_by_lua_file ‘/etc/nginx/authenticate.lua’;

location ~ ^(?<script_name>.+\.php)(?<path_info>/.*)?$ {
try_files $script_name =404;
fastcgi_pass unix:/var/run/php-fpm.sock;
include fastcgi_params;
}

try_files $uri $uri/ /index.php$is_args$args;
}

…then the authentication process works only for the path /restricted; it doesn’t work for the paths under the tree such as /restricted/a. I haven’t been able to find the exact reason to this but I have a naive fix for it:

local auth_groups = {
[“group1”] = {“/restricted”};
[“group2”] = {“/restricted2”};
}

local user_group

for groupname, urls in pairs(auth_groups) do
for _, url in pairs(urls) do
if ngx.var.request_uri:find(url) then
user_group = groupname
break
end
end
end

Run this piece of code after the basic configuration of the authentication script. This is naive because it does a linear (sequential) search in all the groups for every request. If you are going to use this on a large scale, a linear search can quickly eat up your CPU resources.
This piece of code changes the Nginx part of the configuration slightly. We were earlier sending $user_group from Nginx; now we don’t have to, as the Lua script will verify the URL to group mapping, as follows:

location /phpMyAdmin {
root /usr/local/www;
access_by_lua_file ‘authenticate.lua’;

location ~ ^(?<script_name>.+\.php)(?<path_info>/.*)?$ {
try_files $script_name =404;
fastcgi_pass unix:/var/run/php-fpm-www.sock;
include fastcgi_params;
}
}

The authentication script is available on my GitHub account: https://github.com/nileshgr/utilities/blob/master/admin/nginx-group-authenticate.lua.

1 COMMENT

  1. […] this and use the old algorithm of HTTP. Old HTTP is not quite effective these days as there are a lot of ways to work things around it. However, HTTPs is still a very secure and gives you the privacy to hide your […]

LEAVE A REPLY

Please enter your comment!
Please enter your name here