home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


Writing Apache Modules with Perl and C
By:   Lincoln Stein and Doug MacEachern
Published:   O'Reilly & Associates, Inc.  - March 1999

Copyright © 1999 by O'Reilly & Associates, Inc.


 


   Show Contents   Previous Page   Next Page

Chapter 6 - Authentication and Authorization / Authentication Handlers
Authenticating Against a Database

Let's turn to systems that check the user's identity against a database. We debated a bit about what type of authentication database to use for these examples. Candidates included the Unix password file, the Network Information System (NIS), and Bellcore's S/Key one-time password system, but we decided that these were all too Unix-specific. So we turned back to the DBI abstract database interface, which at least is portable across Windows and Unix systems.

Chapter 5, Maintaining State, talked about how the DBI interface works, and showed how to use Apache::DBI to avoid opening and closing database sessions with each connection. For a little variety, we'll use Tie::DBI in this chapter. It's a simple interface to DBI database tables that makes them look like hashes. For example, here's how to tie variable %h to a MySQL database named test_www:

  tie %h, 'Tie::DBI', {
       db    => 'mysql:test_www',
       table => 'user_info',
       key   => 'user_name',
  };

The options that can be passed to tie() include db for the database source string or a previously opened database handle, table for the name of the table to bind to (in this case, user_info), and key for the field to use as the hash key (in this case, user_name). Other options include authentication information for logging into the database. After successfully tying the hash, you can now access the entire row keyed by username fred like this:

  $record = $h{'fred'}

and the passwd column of the row like this:

  $password = $h{'fred'}{'passwd'};

Because %h is tied to the Tie::DBI class, all stores and retrievals are passed to Tie::DBI methods which are responsible for translating the requested operations into the appropriate SQL queries.

In our examples we will be using a MySQL database named test_www. It contains a table named user_info with the following structure:

+-----------+---------------+-------+---------------------+
| user_name | passwd        | level | groups              |
+-----------+---------------+-------+---------------------+
| fred      | 8uUnFnRlW18qQ |     2 | users,devel         |
| andrew    | No9eULpnXZAjY |     2 | users               |
| george    | V8R6zaQuOAWQU |     3 | users               |
| winnie    | L1PKv.rN0UmsQ |     3 | users,authors,devel |
| root      | UOY3rvTFXJAh2 |     5 | users,authors,admin |
| morgana   | 93EhPjGSTjjqY |     1 | users               |
+-----------+---------------+-------+---------------------+

The password field is encrypted with the Unix crypt() call, which conveniently enough is available to Perl scripts as a built-in function call. The level column indicates the user's level of access to the site (higher levels indicate more access). The groups field provides a comma-delimited list of groups that the user belongs to, providing another axis along which we can perform authorization. These will be used in later examples.

Tie::DBI is not a standard part of Perl. If you don't have it, you can find it at CPAN in the modules subdirectory. You'll also need the DBI (database interface) module and a DBD (Database Driver) module for the database of your choice.

For the curious, the script used to create this table and its test data are given in Example 6-7. We won't discuss it further here.

Example 6-7. Creating the Test DBI Table

#!/usr/local/bin/perl
use strict;
use Tie::DBI ();
my $DB_NAME = 'test_www';
my $DB_HOST = 'localhost';
my %test_users = (
                #user_name        groups            level   passwd
                'root'   =>  [qw(users,authors,admin  5     superman)],
                'george'  => [qw(users                3     jetson)],
'winnie' => [qw(users,authors,devel 3 thepooh)], 'andrew' => [qw(users 2 llama23)], 'fred' => [qw(users,devel 2 bisquet)], 'morgana' => [qw(users 1 lafey)] );
# Sometimes it's easier to invoke a subshell for simple things
# than to use the DBI interface.
open MYSQL, "|mysql -h $DB_HOST -f $DB_NAME" or die $!;
print MYSQL <<END;
   DROP TABLE user_info;
CREATE TABLE user_info (
                      user_name   CHAR(20) primary key,
                      passwd      CHAR(13) not null,
                      level       TINYINT  not null,
                      groups      CHAR(100)
                      );
END
close MYSQL;
tie my %db, 'Tie::DBI', {
   db => "mysql:$DB_NAME:$DB_HOST",
   table => 'user_info',
   key   => 'user_name',
   CLOBBER=>1,
} or die "Couldn't tie to $DB_NAME:$DB_HOST";
my $updated = 0;
for my $id (keys %test_users) {
   my($groups, $level, $passwd) = @{$test_users{$id}};
   $db{$id} = {
      passwd  =>  crypt($passwd, salt()),
      level   =>  $level,
      groups  =>  $groups,
   };
   $updated++;
}
untie %db;
print STDERR "$updated records entered.\n";
# Possible BUG: Assume that this system uses two character
# salts for its crypt().
sub salt {
   my @saltset = (0..9, 'A'..'Z', 'a'..'z', '.', '/');
   return join '', @saltset[rand @saltset, rand @saltset];
}

To use the database for user authentication, we take the skeleton from Apache::AuthAny and flesh it out so that it checks the provided username and password against the corresponding fields in the database. The complete code for Apache::AuthTieDBI and a typical configuration file entry are given in Example 6-8.

The handler() subroutine is succinct:

sub handler {
   my $r = shift;
    # get user's authentication credentials
   my($res, $sent_pw) = $r->get_basic_auth_pw;
   return $res if $res != OK;
   my $user = $r->connection->user;
   my $reason = authenticate($r, $user, $sent_pw);
   if($reason) {
      $r->note_basic_auth_failure;
      $r->log_reason($reason, $r->filename);
      return AUTH_REQUIRED;
   }
   return OK;
}

The routine begins like the previous authentication modules by fetching the user's password from get_basic_auth_pw() and username from $r->connection->user. If successful, it calls an internal subroutine named authenticate() with the request object, username, and password. authenticate() returns undef on success or an error message on failure. If an error message is returned, we log the error and return AUTH_REQUIRED. Otherwise, we return OK.

Most of the interesting stuff happens in the authenticate() subroutine:

sub authenticate {
   my($r, $user, $sent_pw) = @_;
    # get configuration information
   my $dsn        = $r->dir_config('TieDatabase') || 'mysql:test_www';
   my $table_data = $r->dir_config('TieTable')    || 'users:user:passwd';
   my($table, $userfield, $passfield) = split ':', $table_data;
    $user && $sent_pw or return 'empty user names and passwords disallowed';

Apache::AuthTieDBI relies on two configuration variables to tell it where to look for authentication information: TieDatabase indicates what database to use in standard DBI Data Source Notation. TieTable indicates what database table and fields to use, in the form ::. If these configuration variables aren't present, the module uses various hardcoded defaults. At this point the routine tries to establish contact with the database by calling tie():

    tie my %DB, 'Tie::DBI', {
       db => $dsn, table => $table, key => $userfield,
   } or return "couldn't open database";

Provided that the Apache::DBI module was previously loaded (see the section "Storing State Information in SQL Databases" in Chapter 5), the database handle will be cached behind the scenes and there will be no significant overhead for calling tie() once per transaction. Otherwise it would be a good idea to cache the tied %DB variable and reuse it as we've done in other modules. We've assumed in this example that the database itself doesn't require authentication. If this isn't the case on your system, modify the call to tie() to include the user and password options:

tie my %DB, 'Tie::DBI', {
 db => $dsn, table => $table, key => $userfield,
 user => 'aladdin', password => 'opensesame'
} or return "couldn't open database";

Replace the username and password shown here with values that are valid for your database.

The final steps are to check whether the provided user and password are valid:

    $DB{$user} or return "invalid account";
   my $saved_pw = $DB{$user}{$passfield};
   $saved_pw eq crypt($sent_pw, $saved_pw) or return "password mismatch";
    # if we get here, all is well
   return "";
}

The first line of this chunk checks whether $user is listed in the database at all. The second line recovers the password from the tied hash, and the third line calls crypt() to compare the current password to the stored one.

In case you haven't used crypt() before, it takes two arguments, the plain text password and a two- or four-character "salt" used to seed the encryption algorithm. Different salts yield different encrypted passwords.7 The returned value is the encrypted password with the salt appended at the beginning. When checking a plain-text password for correctness, it's easiest to use the encrypted password itself as the salt. crypt() will use the first few characters as the salt and ignore the rest. If the newly encrypted value matches the stored one, then the user provided the correct plain-text password.

If the encrypted password matches the saved password, we return an empty string to indicate that the checks passed. Otherwise, we return an error message.

Example 6-8. Apache::AuthTieDBI authenticates against a DBI database

package Apache::AuthTieDBI;
use strict;
use Apache::Constants qw(:common);
use Tie::DBI ();
sub handler {
   my $r = shift;
    # get user's authentication credentials
   my($res, $sent_pw) = $r->get_basic_auth_pw;
   return $res if $res != OK;
   my $user = $r->connection->user;
    my $reason = authenticate($r, $user, $sent_pw);
    if($reason) {
      $r->note_basic_auth_failure;
      $r->log_reason($reason, $r->filename);
      return AUTH_REQUIRED;
   }
   return OK;
}
sub authenticate {
   my($r, $user, $sent_pw) = @_;
    # get configuration information
   my $dsn        = $r->dir_config('TieDatabase') || 'mysql:test_www';
   my $table_data = $r->dir_config('TieTable')    || 'users:user:passwd';
   my($table, $userfield, $passfield) = split ':', $table_data;
    $user && $sent_pw or return 'empty user names and passwords disallowed';
    tie my %DB, 'Tie::DBI', {
      db => $dsn, table => $table, key => $userfield,
   } or return "couldn't open database";
    $DB{$user} or return "invalid account";
    my $saved_pw = $DB{$user}{$passfield};
   $saved_pw eq crypt($sent_pw, $saved_pw) or return "password mismatch";
    # if we get here, all is well
   return "";
}
1;
__END__

A configuration file entry to go along with Apache::AuthTieDBI:

<Location /registered_users>
  AuthName "Registered Users"
  AuthType Basic
  PerlAuthenHandler Apache::AuthTieDBI
   PerlSetVar       TieDatabase  mysql:test_www
  PerlSetVar       TieTable     user_info:user_name:passwd
   require valid-user
</Location>

The next section builds on this example to show how the other fields in the tied database can be used to implement a customizable authorization scheme.

Footnotes

7 The salt is designed to make life a bit harder for password-cracking programs that use a dictionary to guess the original plain-text password from the encrypted password. Because there are 4,096 different two-character salts, this increases the amount of disk storage the cracking program needs to store its dictionary by three orders of magnitude. Unfortunately, now that high-capacity disk drives are cheap, this is no longer as much an obstacle as it used to be.

   Show Contents   Previous Page   Next Page
Copyright © 1999 by O'Reilly & Associates, Inc.