Group Authentication For Nginx

Nginx’s access control system lacks an equivalent for Apache’s group based access control. But it is easy to emulate this Apache feature with a little bit of scripting.

In order to make the browser pop up an ugly username and password dialog before users can enter your site, you have to configure something like this:

1
2
3
4
location /protected {
    auth_basic "My Own Private NSA";
    auth_basic_user_file /path/to/auth/htusers;
}

The password file /path/to/auth/htusers can be created with the Apache htpasswd tool or with the python script htpasswd.py. The result will look like this:

1
2
3
4
5
6
$ cat /path/to/auth/htusers
hihosilver:$apr1$7iHB6eAM$bhrCoWXyXVDHXYo4aXZlf.
tom:$apr1$k03jYOif$hcf8rXosgreIz0v5oAtqD0
dick:$apr1$ZtrDNWP4$1G7sOMKOGZuBYdCu37bSX.
harry:$apr1$Lv1OSEIx$HHNvUurRBs/IRtSuYGXMh/
superuser:$apr1$wh3lz3Zw$TW4BO3mhEcWkFwcx.fZ760

For Apache the same protection can be set up like this:

1
2
3
4
5
6
<Location /protected>
    AuthType Basic
    AuthName "My Own Private NSA"
    AuthUserFile "/path/to/auth/htusers"
    Require valid-user
</Location>

The directive Require valid-user means “any user that can be found in AuthUserFile”.

Group Based Authentication, the Simple Way

But what if there is an area /protected/admin that should only be accessible for the users hihosilver and superuser? In Apache that is a piece of cake:

1
2
3
4
5
6
7
<Location /protected/admin>
    AuthType Basic
    AuthName "My Own Private NSA"
    AuthUserFile "/path/to/auth/htusers"
    AuthGroupFile "/path/to/auth/htgroups"
    Require group admin
</Location>

There is now a group file (line 5) and not every user but only users from the group admin are granted access. The group file can be created with any text editor and looks like this:

1
2
3
$ cat /path/to/auth/htgroups
admin: superuser hihosilver
users: superuser hihosilver tom dick harry

Only superuser and hihosilver are members of the group admin and only they are allowed in.

Unfortunately, nginx does not know about groups. The only thing we can do is create a second password file:

1
2
3
4
location /protected/admin {
    auth_basic "My Own Private NSA";
    auth_basic_user_file /path/to/auth/admin.users;
}

Same as above for /protected but we specify a different password file (line 3). We could create second the password file like this:

1
2
$ cd /path/to/auth
$ grep 'hihosilver|superuser:' htusers >admin.users

That copies the password information for the users hihosilver and superuser. Unfortunately, it also copies lines that contain one of these strings in the password digest (or the user name). The regular expression has to be less permissive:

1
2
$ cd /path/to/auth
$ grep -E '^(hihosilver|superuser):' htusers >admin.users

This will be sufficient for many setups. Be sure though to use the same authentication realm (that is the keyword auth_basic in the nginx configuration) for all areas. Otherwise, admin users will be prompted again for the password, when they visit the admin area.

Group Based Authentication, the Comfortable Way

The solution with grep will soon become impractical if you have more groups and more areas to protect. An apache-style group file would definitely come in handy and you can have that for nginx as well:

Download the script nginx-groups.pl and save it in the directory /path/to/auth. Create the group file htgroups (see above) and run the script:

1
2
3
$ perl nginx-groups.pl htusers htgroups 
Writing users file 'admin.users'.
Writing users file 'users.users'.

The script reads the files with the user and group information and generates one file GROUP.users for each group GROUP.

The script is very basic and limited. For example, it always writes the output files into the current working directory, the naming scheme GROUP.users is hard-coded but it does the job it is supposed to do.

Feel free to change it to your individual needs. It is easy to understand:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#! /usr/bin/env perl

use strict;

die "Usage: $0 USERSFILE GROUPSFILE\n" unless @ARGV == 2;
my ($users_file, $groups_file) = @ARGV;

my %users;
open my $fh, "<$users_file" or die "cannot open '$users_file': $!\n";
while (my $line = <$fh>) {
    chomp $line;
    my ($name, $password) = split /:/, $line, 2;
    next if !defined $password;
    $users{$name} = $line;
}

open my $fh, "<$groups_file" or die "cannot open '$groups_file': $!\n";
while (my $line = <$fh>) {
    my ($name, $members) = split /:/, $line, 2 or next;
    next if !defined $members;
    $name =~ s/[ \t]//g;
    next if $name eq '';
    my @members = grep { length $_ && exists $users{$_} } 
                  split /[ \t\r\n]+/, $members;
    
    my $groups_users_file = $name . '.users';

    print "Writing users file '$groups_users_file'.\n";

    open my $wh, ">$groups_users_file" 
        or die "Cannot open '$groups_users_file' for writing: $!\n";

    foreach my $user (@members) {
        print $wh "$users{$user}\n"
            or die "Cannot write to '$groups_users_file': $!\n";
    }

    close $wh or die "Cannot close '$groups_users_file': $!\n";
}

In lines 8 to 15, the user file is read and each valid line is stored in a hash (dictionary, associative array or whatever you call it) using the user name as the key.

Then the groups file is read in a similar manner, and (line 30) a dedicated password file for each group is written. It is important that a password file is also (over)written, when the group has no members.
Otherwise a stale password file would be used by nginx granting unwanted access.


blog comments powered by Disqus