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. |