Apache::AuthDigestDBI - Authentication and Authorization via Perl's DBI,


Apache-AuthDigest documentation Contained in the Apache-AuthDigest distribution.

Index


Code Index:

NAME

Top

Apache::AuthDigestDBI - Authentication and Authorization via Perl's DBI, supporting both Basic and Digest Authentication

SYNOPSIS

Top

 # Configuration in httpd.conf or startup.pl:

 PerlModule Apache::AuthDigestDBI

 # Authentication and Authorization in .htaccess:

 AuthName DBI
 AuthType Digest

 PerlAuthenHandler Apache::AuthDigestDBI::authen
 PerlAuthzHandler  Apache::AuthDigestDBI::authz

 PerlSetVar Auth_DBI_data_source   dbi:driver:dsn
 PerlSetVar Auth_DBI_username      db_username
 PerlSetVar Auth_DBI_password      db_password
 #DBI->connect($data_source, $username, $password)

 PerlSetVar Auth_DBI_pwd_table     users
 PerlSetVar Auth_DBI_uid_field     username
 PerlSetVar Auth_DBI_pwd_field     password
 # authentication: SELECT pwd_field FROM pwd_table WHERE uid_field=$user
 PerlSetVar Auth_DBI_grp_field     groupname
 # authorization: SELECT grp_field FROM pwd_table WHERE uid_field=$user

 require valid-user
 require user   user_1  user_2 ...
 require group group_1 group_2 ...

The AuthType may be Digest or Basic. It will 'fallback' to Basic if the client ignores the request for Digest authentication. The password must not be encrypted for Digest authentication and the fallback to Basic. For Basic authentication, passwords may be encrypted.

You may use one or more valid require lines. For a single require line with the requirement 'valid-user' or with the requirements 'user user_1 user_2 ...' it is sufficient to use only the authentication handler.

DESCRIPTION

Top

This is a hacked up version Apache::AuthDBI that uses Apache::AuthDigest to do Digest authentication. Please see the docs for Apache::AuthDBI for full usage.

PREREQUISITES

Top

Note that this module requires Apache::AuthDBI and Apache::AuthDigest.

SEE ALSO

Top

Apache::AuthDBI, Apache::AuthDigest::API, Apache, mod_perl, DBI

BUGS

Top

The password must not be encrypted for use with Digest authentication.

When Digest authentication is requested, it accepts Basic authentication. (This isn't a bug, except that you cannot shut this behavior off.)

AUTHORS

Top

COPYRIGHT

Top


Apache-AuthDigest documentation Contained in the Apache-AuthDigest distribution.

package Apache::AuthDigestDBI;

use Apache ();
use Apache::Constants qw( OK AUTH_REQUIRED FORBIDDEN DECLINED SERVER_ERROR );
use DBI ();
use IPC::SysV qw( IPC_CREAT IPC_RMID S_IRUSR S_IWUSR );
use Apache::AuthDigest::API;
use Digest::MD5;
use strict;

# $Id: AuthDigestDBI.pm,v 1.1 2002/11/11 13:58:37 geoff Exp $

require_version DBI 1.00;

$Apache::AuthDigestDBI::VERSION = '0.89';

# 1: report about cache miss
# 2: full debug output
$Apache::AuthDigestDBI::DEBUG = 0;


# configuration attributes, defaults will be overwritten with values from .htaccess.

my %Config = (
    'Auth_DBI_data_source'      => '',
    'Auth_DBI_username'         => '',
    'Auth_DBI_password'         => '',
    'Auth_DBI_pwd_table'        => '',
    'Auth_DBI_uid_field'        => '',
    'Auth_DBI_pwd_field'        => '',
    'Auth_DBI_pwd_whereclause'  => '',
    'Auth_DBI_grp_table'        => '',
    'Auth_DBI_grp_field'        => '',
    'Auth_DBI_grp_whereclause'  => '',
    'Auth_DBI_log_field'        => '',
    'Auth_DBI_log_string'       => '',
    'Auth_DBI_authoritative'    => 'on',
    'Auth_DBI_nopasswd'         => 'off',
    'Auth_DBI_encrypted'        => 'on',
    'Auth_DBI_encryption_salt'  => 'password',
    'Auth_DBI_uidcasesensitive' => 'on',
    'Auth_DBI_pwdcasesensitive' => 'on',
    'Auth_DBI_placeholder'      => 'off',
);

# stores the configuration of current URL.
# initialized  during authentication, eventually re-used for authorization.
my $Attr = { };


# global cache: all records are put into one string.
# record separator is a newline. Field separator is $;.
# every record is a list of id, time of last access, password, groups (authorization only).
# the id is a comma separated list of user_id, data_source, pwd_table, uid_field.
# the first record is a timestamp, which indicates the last run of the CleanupHandler followed by the child counter.

my $Cache = time . "$;0\n";

# unique id which serves as key in $Cache.
# the id is generated during authentication and re-used for authorization.
my $ID;


# minimum lifetimes of cache entries in seconds.
# setting the CacheTime to 0 will not use the cache at all.

my $CacheTime = 0;

# supposed to be called in a startup script.
# sets CacheTime to a user defined value.

sub setCacheTime {
    my $class      = shift;
    my $cache_time = shift;
    # sanity check
    $CacheTime = $cache_time if ($cache_time =~ /\d+/);
}


# minimum time interval in seconds between two runs of the PerlCleanupHandler.
# setting CleanupTime to 0 will run the PerlCleanupHandler after every request.
# setting CleanupTime to a negative value will disable the PerlCleanupHandler.

my $CleanupTime = -1;

# supposed to be called in a startup script.
# sets CleanupTime to a user defined value.

sub setCleanupTime {
    my $class        = shift;
    my $cleanup_time = shift;
    # sanity check
    $CleanupTime = $cleanup_time if ($cleanup_time =~ /\-*\d+/);
}


# optionally the string with the global cache can be stored in a shared memory segment.
# the segment will be created from the first child and it will be destroyed if the last child exits.
# the reason for not handling everything in the main server is simply, that there is no way to setup 
# an ExitHandler which runs in the main server and which would remove the shared memory and the semaphore.
# hence we have to keep track about the number of children, so that the last one can do all the cleanup.
# creating the shared memory in the first child also has the advantage, that we don't have to cope
# with changing the ownership.
# if a shm-function fails, the global cache will automatically fall back to one string per process.

my $SHMKEY  =     0; # unique key for shared memory segment and semaphore set
my $SEMID   =     0; # id of semaphore set
my $SHMID   =     0; # id of shared memory segment
my $SHMSIZE = 50000; # default size of shared memory segment

# shortcuts for semaphores
my $obtain_lock  = pack("sss", 0,  0, 0) . pack("sss", 0, 1, 0);
my $release_lock = pack("sss", 0, -1, 0);

# supposed to be called in a startup script.
# sets SHMSIZE to a user defined value and initializes the unique key, used for the shared memory segment and for the semaphore set.
# creates a PerlChildInitHandler which creates the shared memory segment and the semaphore set.
# creates a PerlChildExitHandler which removes the shared memory segment and the semaphore set upon server shutdown.
# keep in mind, that this routine runs only once, when the main server starts up.

sub initIPC {
    my $class   = shift;
    my $shmsize = shift;

    # make sure, this method is called only once
    return if $SHMKEY;

    # ensure minimum size of shared memory segment
    $SHMSIZE = $shmsize if $shmsize >= 500;

    # generate unique key based on path of AuthDBI.pm
    foreach my $file (keys %INC) {
        if ($file eq 'Apache/AuthDBI.pm') {
            $SHMKEY = IPC::SysV::ftok($INC{$file}, 1);
            last;
        }
    }

    # provide a handler which initializes the shared memory segment (first child)
    # or which increments the child counter. 
    if(Apache->can('push_handlers')) {
        Apache->push_handlers("PerlChildInitHandler" => \&childinit);
    }

    # provide a handler which decrements the child count or which destroys the shared memory 
    # segment upon server shutdown, which is defined by the exit of the last child.
    if(Apache->can('push_handlers')) {
        Apache->push_handlers("PerlChildExitHandler" => \&childexit);
    }
}


# authentication handler

sub authen {

    my ($r) = @_;
    my ($key, $val, $dbh);

    my $prefix = "$$ Apache::AuthDigestDBI::authen";

    if ($Apache::AuthDigestDBI::DEBUG > 1) {
        my ($type) = '';
        $type .= 'initial ' if $r->is_initial_req;
        $type .= 'main'     if $r->is_main;
        print STDERR "==========\n$prefix request type = >$type< \n";
    }

    return OK unless $r->is_initial_req; # only the first internal request

    print STDERR "REQUEST:\n", $r->as_string if $Apache::AuthDigestDBI::DEBUG > 1;
	
	my $auth = 'digest';

	# here the dialog pops up and asks you for username and password
	my ($status, $response, $res, $passwd_sent);
	if ($r->header_in("Authorization") =~ /^Basic (.*)/i) {
		$auth = 'Basic';
		my $username;
		($username, $passwd_sent) = split ':', old_decode_base64($1);
		$r->connection->user($username);
	}

	if ($auth eq 'digest') {

		$r = Apache::AuthDigest::API->new($r);
		($status, $response) = $r->get_digest_auth_response;		
		return $status unless $status == OK;
		$passwd_sent = 'digest';
		
	} else {
	
		#($res, $passwd_sent) = $r->get_basic_auth_pw;
		#print STDERR "$prefix get_basic_auth_pw: res = >$res<, password sent = >$passwd_sent<\n" if $Apache::AuthDigestDBI::DEBUG > 1;
		#return $res if $res; # e.g. HTTP_UNAUTHORIZED
		
		return AUTH_REQUIRED unless $passwd_sent;
		
	}

    # get username
    my ($user_sent) = $r->connection->user;
    print STDERR "$prefix user sent = >$user_sent<\n" if $Apache::AuthDigestDBI::DEBUG > 1;

    # do we use shared memory for the global cache ?
    print STDERR "$prefix cache in shared memory, shmid $SHMID, shmsize $SHMSIZE, semid $SEMID \n" if ($SHMID and $Apache::AuthDigestDBI::DEBUG > 1);

    # get configuration
    while(($key, $val) = each %Config) {
        $val = $r->dir_config($key) || $val;
        $key =~ s/^Auth_DBI_//;
        $Attr->{$key} = $val;
        printf STDERR "$prefix Config{ %-16s } = %s\n", $key, $val if $Apache::AuthDigestDBI::DEBUG > 1;
    }

    # parse connect attributes, which may be tilde separated lists
    my @data_sources = split(/~/, $Attr->{data_source});
    my @usernames    = split(/~/, $Attr->{username});
    my @passwords    = split(/~/, $Attr->{password});
    $data_sources[0] = '' unless $data_sources[0]; # use ENV{DBI_DSN} if not defined

    # obtain the id for the cache
    my $data_src = $Attr->{data_source};
    $data_src =~ s/\(.+\)//go; # remove any embedded attributes, because of trouble with regexps
    $ID = join ',', $user_sent, $data_src, $Attr->{pwd_table}, $Attr->{uid_field};

    # if not configured decline
    unless ($Attr->{pwd_table} && $Attr->{uid_field} && $Attr->{pwd_field}) {
        printf STDERR "$prefix not configured, return DECLINED\n" if $Apache::AuthDigestDBI::DEBUG > 1;
        return DECLINED;
    }

    # do we want Windows-like case-insensitivity?
    $user_sent   = lc($user_sent)   if $Attr->{uidcasesensitive} eq "off";
    $passwd_sent = lc($passwd_sent) if $Attr->{pwdcasesensitive} eq "off";

    # check whether the user is cached but consider that the password possibly has changed
    my $passwd = '';
    my $salt   = '';
    if ($CacheTime) { # do we use the cache ?
        if ($SHMID) { # do we keep the cache in shared memory ?
            semop($SEMID, $obtain_lock) or print STDERR "$prefix semop failed \n";
            shmread($SHMID, $Cache, 0, $SHMSIZE) or printf STDERR "$prefix shmread failed \n";
            substr($Cache, index($Cache, "\0")) = '';
            semop($SEMID, $release_lock) or print STDERR "$prefix semop failed \n";
        }
        # find id in cache
        my ($last_access, $passwd_cached, $groups_cached);
        if ($Cache =~ /$ID$;(\d+)$;(.+)$;(.*)\n/) {
            $last_access   = $1;
            $passwd_cached = $2;
            $groups_cached = $3;
            printf STDERR "$prefix cache: found >$ID< >$last_access< >$passwd_cached< \n" if $Apache::AuthDigestDBI::DEBUG > 1;
			if ($auth eq 'digest') {
				$salt = $response->{'realm'};
				my $passwd_to_check = Digest::MD5::md5_hex(join ':', $user_sent, $salt, $passwd_cached);
				$passwd = $passwd_cached if $r->compare_digest_response($response, $passwd_to_check);
			} else {
				$salt = $Attr->{encryption_salt} eq 'userid' ? $user_sent : $passwd_cached;
				my $passwd_to_check = $Attr->{encrypted} eq 'on' ? crypt($passwd_sent, $salt) : $passwd_sent; 
				# match cached password with password sent 
				$passwd = $passwd_cached if $passwd_to_check eq $passwd_cached;
			}
        }
    }

    if ($passwd) { # found in cache
        printf STDERR "$prefix passwd found in cache \n" if $Apache::AuthDigestDBI::DEBUG > 1;
    } else { # password not cached or changed
        printf STDERR "$prefix passwd not found in cache \n" if $Apache::AuthDigestDBI::DEBUG;

        # connect to database, use all data_sources until the connect succeeds
        my $j;
        for ($j = 0; $j <= $#data_sources; $j++) {
            last if ($dbh = DBI->connect($data_sources[$j], $usernames[$j], $passwords[$j]));
        }
        unless ($dbh) {
            $r->log_reason("$prefix db connect error with data_source >$Attr->{data_source}<", $r->uri);
            return SERVER_ERROR;
        }

        # generate statement
        my $user_sent_quoted = $dbh->quote($user_sent);
        my $select    = "SELECT $Attr->{pwd_field}";
        my $from      = "FROM $Attr->{pwd_table}";
        my $where     = ($Attr->{uidcasesensitive} eq "off") ? "WHERE lower($Attr->{uid_field}) =" : "WHERE $Attr->{uid_field} =";
        my $compare   = ($Attr->{placeholder}      eq "on")  ? "?" : "$user_sent_quoted";
        my $statement = "$select $from $where $compare";
        $statement   .= " AND $Attr->{pwd_whereclause}" if $Attr->{pwd_whereclause};
        print STDERR "$prefix statement: $statement\n" if $Apache::AuthDigestDBI::DEBUG > 1;

        # prepare statement
        my $sth;
        unless ($sth = $dbh->prepare($statement)) {
            $r->log_reason("$prefix can not prepare statement: $DBI::errstr", $r->uri);
            $dbh->disconnect;
            return SERVER_ERROR;
        }

        # execute statement
        my $rv;
        unless ($rv = ($Attr->{placeholder} eq "on") ? $sth->execute($user_sent) : $sth->execute) {
            $r->log_reason("$prefix can not execute statement: $DBI::errstr", $r->uri);
            $dbh->disconnect;
            return SERVER_ERROR;
        }

        # fetch result
        while ($_ = $sth->fetchrow_array) {
            # strip trailing blanks for fixed-length data-type
            $_ =~ s/ +$// if $_;
            # consider the case with many users sharing the same userid
	    $passwd .= "$_$;";
        }

        chop  $passwd if $passwd;
        undef $passwd if 0 == $sth->rows; # so we can distinguish later on between no password and empty password

        if ($sth->err) {
            $dbh->disconnect;
            return SERVER_ERROR;
        }
        $sth->finish;

        # re-use dbh for logging option below
        $dbh->disconnect unless ($Attr->{log_field} && $Attr->{log_string});
    }

    $r->subprocess_env(REMOTE_PASSWORDS => $passwd);
    print STDERR "$prefix passwd = >$passwd<\n" if $Apache::AuthDigestDBI::DEBUG > 1;

    # check if password is needed
    if (!defined($passwd)) { # not found in database
        # if authoritative insist that user is in database
        if ($Attr->{authoritative} eq 'on') {
            $r->log_reason("$prefix password for user $user_sent not found", $r->uri);
            $r->note_basic_auth_failure;
            return AUTH_REQUIRED;
        } else {
            # else pass control to the next authentication module
            return DECLINED;
        }
    }

    # allow any password if nopasswd = on and the retrieved password is empty
    if ($Attr->{nopasswd} eq 'on' && !$passwd) {
        return OK;
    }

    # if nopasswd is off, reject user
    unless ($passwd_sent && $passwd) {
        $r->log_reason("$prefix user $user_sent: empty password(s) rejected", $r->uri);
        $r->note_basic_auth_failure;
        return AUTH_REQUIRED;
    }

    # compare passwords
    my $found = 0;
    my $password;
    foreach $password (split(/$;/, $passwd)) {
        # compare the two passwords possibly crypting the password if needed
        my $did_match = 0;
		if ($auth eq 'digest') {
			$salt = $response->{'realm'};
			# password to check is in a reverse role from below
			# it's the correct password
			my $passwd_to_check = Digest::MD5::md5_hex(join ':', $user_sent, $salt, $password);
			$did_match = 1 if $r->compare_digest_response($response, $passwd_to_check);
		} else {
			$salt = $Attr->{encryption_salt} eq 'userid' ? $user_sent : $password;
			my $passwd_to_check = $Attr->{encrypted} eq 'on' ? crypt($passwd_sent, $password) : $passwd_sent;
            print STDERR "$prefix user $user_sent: > '$passwd_to_check' eq '$password' < \n" if $Apache::AuthDigestDBI::DEBUG > 1;
			$did_match = 1 if $passwd_to_check eq $password;
		}
		
        if ($did_match) {
            $found = 1;
            $r->subprocess_env(REMOTE_PASSWORD => $password);
            print STDERR "$prefix user $user_sent: password match for >$password< \n" if $Apache::AuthDigestDBI::DEBUG > 1;
            # update timestamp and cache userid/password if CacheTime is configured
            if ($CacheTime) { # do we use the cache ?
                if ($SHMID) { # do we keep the cache in shared memory ?
                    semop($SEMID, $obtain_lock) or print STDERR "$prefix semop failed \n";
                    shmread($SHMID, $Cache, 0, $SHMSIZE) or printf STDERR "$prefix shmread failed \n";
                    substr($Cache, index($Cache, "\0")) = '';
                }
                # update timestamp and password or append new record
                my $now = time;
                if (!($Cache =~ s/$ID$;\d+$;.*$;(.*)\n/$ID$;$now$;$password$;$1\n/)) {
		    $Cache .= "$ID$;$now$;$password$;\n";
                } else {
                }
                if ($SHMID) { # write cache to shared memory
                    shmwrite($SHMID, $Cache, 0, $SHMSIZE)  or printf STDERR "$prefix shmwrite failed \n";
                    semop($SEMID, $release_lock) or print STDERR "$prefix semop failed \n";
                }
            }
            last;
        }
    }
    unless ($found) {
        $r->log_reason("$prefix user $user_sent: password mismatch", $r->uri);
		if ($auth eq 'digest') {
			$r->note_digest_auth_failure;
		} else {
			$r->note_basic_auth_failure;		
		}
        return AUTH_REQUIRED;
    }

    # logging option
    if ($Attr->{log_field} && $Attr->{log_string}) {
        if (!$dbh) { # connect to database if not already done
            my ($j, $connect);
            for ($j = 0; $j <= $#data_sources; $j++) {
                if ($dbh = DBI->connect($data_sources[$j], $usernames[$j], $passwords[$j])) {
                    $connect = 1;
                    last;
                }
            }
            unless ($connect) {
                $r->log_reason("$prefix db connect error with $Attr->{data_source}", $r->uri);
                return SERVER_ERROR;
            }
        }
        my $user_sent_quoted = $dbh->quote($user_sent);
        my $statement = "UPDATE $Attr->{pwd_table} SET $Attr->{log_field} = $Attr->{log_string} WHERE $Attr->{uid_field}=$user_sent_quoted";
        print STDERR "$prefix statement: $statement\n" if $Apache::AuthDigestDBI::DEBUG > 1;
        unless ($dbh->do($statement)) {
            $r->log_reason("$prefix can not do statement: $DBI::errstr", $r->uri);
            $dbh->disconnect;
            return SERVER_ERROR;
        }
        $dbh->disconnect;
    }

    # Unless the cache or the CleanupHandler is disabled, the CleanupHandler is initiated 
    # if the last run was more than $CleanupTime seconds before. 
    # Note, that it runs after the request, hence it cleans also the authorization entries 
    if ($CacheTime and $CleanupTime >= 0) {
        my $diff = time - substr($Cache, 0, index($Cache, "$;"));
        print STDERR "$prefix secs since last CleanupHandler: $diff, CleanupTime: $CleanupTime \n" if $Apache::AuthDigestDBI::DEBUG > 1;
        if ($diff > $CleanupTime and Apache->can('push_handlers')) {
            print STDERR "$prefix push PerlCleanupHandler \n" if $Apache::AuthDigestDBI::DEBUG > 1;
            Apache->push_handlers("PerlCleanupHandler", \&cleanup);
        }
    }

    printf STDERR "$prefix return OK\n" if $Apache::AuthDigestDBI::DEBUG > 1;
    return OK;
}


# authorization handler, it is called immediately after the authentication

sub authz {

    my ($r) = @_;
    my ($key, $val, $dbh);

    my ($prefix) = "$$ Apache::AuthDigestDBI::authz ";

    if ($Apache::AuthDigestDBI::DEBUG > 1) {
        my ($type) = '';
        $type .= 'initial ' if $r->is_initial_req;
        $type .= 'main'     if $r->is_main;
        print STDERR "==========\n$prefix request type = >$type< \n";
    }

    return OK unless $r->is_initial_req; # only the first internal request

    my ($user_result)  = DECLINED;
    my ($group_result) = DECLINED;

    # get username
    my ($user_sent) = $r->connection->user;
    print STDERR "$prefix user sent = >$user_sent<\n" if $Apache::AuthDigestDBI::DEBUG > 1 ;

    # here we could read the configuration, but we re-use the configuration from the authentication

    # parse connect attributes, which may be tilde separated lists
    my @data_sources = split(/~/, $Attr->{data_source});
    my @usernames    = split(/~/, $Attr->{username});
    my @passwords    = split(/~/, $Attr->{password});
    $data_sources[0] = '' unless $data_sources[0]; # use ENV{DBI_DSN} if not defined

    # if not configured decline
    unless ($Attr->{pwd_table} && $Attr->{uid_field} && $Attr->{grp_field}) {
        printf STDERR "$prefix not configured, return DECLINED\n" if $Apache::AuthDigestDBI::DEBUG > 1;
        return DECLINED;
    }

    # do we want Windows-like case-insensitivity?
    $user_sent = lc($user_sent) if $Attr->{uidcasesensitive} eq "off";

    # select code to return if authorization is denied:
    my $authz_denied= $Attr->{expeditive} eq 'on' ? FORBIDDEN : AUTH_REQUIRED;

    # check if requirements exists
    my ($ary_ref) = $r->requires;
    unless ($ary_ref) {
        if ($Attr->{authoritative} eq 'on') {
            $r->log_reason("user $user_sent denied, no access rules specified (DBI-Authoritative)", $r->uri);
            $r->note_basic_auth_failure if $authz_denied == AUTH_REQUIRED;
            return $authz_denied;
        }
        printf STDERR "$prefix no requirements and not authoritative, return DECLINED\n" if $Apache::AuthDigestDBI::DEBUG > 1;
        return DECLINED;
    }

    # iterate over all requirement directives and store them according to their type (valid-user, user, group)
    my($hash_ref, $valid_user, $user_requirements, $group_requirements);
    foreach $hash_ref (@$ary_ref) {
        while (($key,$val) = each %$hash_ref) {
            last if $key eq 'requirement';
        }
        $val =~ s/^\s*require\s+//;
        # handle different requirement-types
        if ($val =~ /valid-user/) {
            $valid_user = 1;
        } elsif ($val =~ s/^user\s+//go) {
            $user_requirements .= " $val";
        } elsif ($val =~ s/^group\s+//go) {
            $group_requirements .= " $val";
        }
    }
    $user_requirements  =~ s/^ //go;
    $group_requirements =~ s/^ //go;
    print STDERR "$prefix requirements: valid-user=>$valid_user< user=>$user_requirements< group=>$group_requirements< \n"  if $Apache::AuthDigestDBI::DEBUG > 1;

    # check for valid-user
    if ($valid_user) {
        $user_result = OK;
        print STDERR "$prefix user_result = OK: valid-user\n" if $Apache::AuthDigestDBI::DEBUG > 1;
    }

    # check for users
    if ($user_result != OK && $user_requirements) {
        $user_result = AUTH_REQUIRED;
        my $user_required;
        foreach $user_required (split /\s+/, $user_requirements) {
            if ($user_required eq $user_sent) {
                print STDERR "$prefix user_result = OK for $user_required \n" if $Apache::AuthDigestDBI::DEBUG > 1;
                $user_result = OK;
                last;
           }
        }
    }

    # check for groups
    if ($user_result != OK && $group_requirements) {
        $group_result = AUTH_REQUIRED;
        my ($group, $group_required);

        # check whether the user is cached but consider that the group possibly has changed
        my $groups = '';
        if ($CacheTime) { # do we use the cache ?
            # we need to get the cached groups for the current id, which has been read already 
            # during authentication, so we do not read the Cache from shared memory again
            my ($last_access, $passwd_cached, $groups_cached);
            if ($Cache =~ /$ID$;(\d+)$;(.*)$;(.+)\n/) {
                $last_access   = $1;
                $passwd_cached = $2;
                $groups_cached = $3;
                printf STDERR "$prefix cache: found >$ID< >$last_access< >$groups_cached< \n" if $Apache::AuthDigestDBI::DEBUG > 1;
                REQUIRE_1: foreach $group_required (split /\s+/, $group_requirements) {
                    foreach $group (split(/,/, $groups_cached)) {
                        if ($group_required eq $group) {
                            $groups = $groups_cached;
                            last REQUIRE_1;
		        }
                    }
                }
            }
        }

        if ($groups) { # found in cache
            printf STDERR "$prefix groups found in cache \n" if $Apache::AuthDigestDBI::DEBUG > 1;
        } else { # groups not cached or changed
            printf STDERR "$prefix groups not found in cache \n" if $Apache::AuthDigestDBI::DEBUG;

            # connect to database, use all data_sources until the connect succeeds
            my ($j, $connect);
            for ($j = 0; $j <= $#data_sources; $j++) {
                if ($dbh = DBI->connect($data_sources[$j], $usernames[$j], $passwords[$j])) {
                    $connect = 1;
                    last;
                }
            }
            unless ($connect) {
                $r->log_reason("$prefix db connect error with $Attr->{data_source}", $r->uri);
                return SERVER_ERROR;
            }

            # generate statement
            my $user_sent_quoted = $dbh->quote($user_sent);
            my $select    = "SELECT $Attr->{grp_field}";
            my $from      = ($Attr->{grp_table}) ? "FROM $Attr->{grp_table}" : "FROM $Attr->{pwd_table}";
            my $where     = ($Attr->{uidcasesensitive} eq "off") ? "WHERE lower($Attr->{uid_field}) =" : "WHERE $Attr->{uid_field} =";
            my $compare   = ($Attr->{placeholder}      eq "on")  ? "?" : "$user_sent_quoted";
            my $statement = "$select $from $where $compare";
            $statement   .= " AND $Attr->{grp_whereclause}" if ($Attr->{grp_whereclause});
            print STDERR "$prefix statement: $statement\n" if $Apache::AuthDigestDBI::DEBUG > 1;

            # prepare statement
            my $sth;
            unless ($sth = $dbh->prepare($statement)) {
                $r->log_reason("can not prepare statement: $DBI::errstr", $r->uri);
                $dbh->disconnect;
                return SERVER_ERROR;
            }

            # execute statement
            my $rv;
            unless ($rv = ($Attr->{placeholder} eq "on") ? $sth->execute($user_sent) : $sth->execute) {
                $r->log_reason("can not execute statement: $DBI::errstr", $r->uri);
                $dbh->disconnect;
                return SERVER_ERROR;
            }

            # fetch result and build a group-list
            my $group;
            while ( $group = $sth->fetchrow_array ) {
                # strip trailing blanks for fixed-length data-type
                $group =~ s/ +$//;
                $groups .= "$group,";
            }
            chop $groups if $groups;

            $sth->finish;
            $dbh->disconnect;
        }

        $r->subprocess_env(REMOTE_GROUPS => $groups);
        print STDERR "$prefix groups = >$groups<\n" if $Apache::AuthDigestDBI::DEBUG > 1;

        # skip through the required groups until the first matches
        REQUIRE_2: foreach $group_required (split /\s+/, $group_requirements) {
            foreach $group (split(/,/, $groups)) {
                # check group
                if ($group_required eq $group) {
                    $group_result = OK;
                    $r->subprocess_env(REMOTE_GROUP => $group);
                    print STDERR "$prefix user $user_sent: group_result = OK for >$group< \n" if $Apache::AuthDigestDBI::DEBUG > 1;
                    # update timestamp and cache userid/groups if CacheTime is configured
                    if ($CacheTime) { # do we use the cache ?
                        if ($SHMID) { # do we keep the cache in shared memory ?
                            semop($SEMID, $obtain_lock) or print STDERR "$prefix semop failed \n";
                            shmread($SHMID, $Cache, 0, $SHMSIZE) or printf STDERR "$prefix shmread failed \n";
                            substr($Cache, index($Cache, "\0")) = '';
                        }
                        # update timestamp and groups
                        my $now = time;
                        # entry must exists from authentication
	        	$Cache =~ s/$ID$;\d+$;(.*)$;.*\n/$ID$;$now$;$1$;$groups\n/;
                        if ($SHMID) { # write cache to shared memory
                            shmwrite($SHMID, $Cache, 0, $SHMSIZE)  or printf STDERR "$prefix shmwrite failed \n";
                            semop($SEMID, $release_lock) or print STDERR "$prefix semop failed \n";
                        }
                    }
                    last REQUIRE_2;
		}
            }
        }
    }

    # check the results of the requirement checks
    if ($Attr->{authoritative} eq 'on' && $user_result != OK && $group_result != OK) {
        my $reason;
        $reason .= " USER"  if $user_result  == AUTH_REQUIRED;
        $reason .= " GROUP" if $group_result == AUTH_REQUIRED;
        $r->log_reason("DBI-Authoritative: Access denied on $reason rule(s)", $r->uri);
        $r->note_basic_auth_failure if $authz_denied == AUTH_REQUIRED;
        return $authz_denied;
    }

    # return OK if authorization was successful
    if ($user_result == OK || $group_result == OK) {
        printf STDERR "$prefix return OK\n" if $Apache::AuthDigestDBI::DEBUG > 1;
        return OK;
    }

    # otherwise fall through
    printf STDERR "$prefix fall through, return DECLINED\n" if $Apache::AuthDigestDBI::DEBUG > 1;
    return DECLINED;
}


# The PerlChildInitHandler initializes the shared memory segment (first child)
# or increments the child counter. 
# Note: this handler runs in every child server, but not in the main server.

sub childinit {
    my $prefix = "$$ Apache::AuthDigestDBI         PerlChildInitHandler";
    # create (or re-use existing) semaphore set
    $SEMID = semget($SHMKEY, 1, IPC_CREAT|S_IRUSR|S_IWUSR);
    if (!defined($SEMID)) {
      print STDERR "$prefix semget failed \n";
      return;
    }
    # create (or re-use existing) shared memory segment
    $SHMID = shmget($SHMKEY, $SHMSIZE, IPC_CREAT|S_IRUSR|S_IWUSR);
    if (!defined($SHMID)) {
      print STDERR "$prefix shmget failed \n";
      return;
    }
    # make ids accessible to other handlers
    $ENV{AUTH_SEMID} = $SEMID;
    $ENV{AUTH_SHMID} = $SHMID;
    # read shared memory, increment child count and write shared memory segment
    semop($SEMID, $obtain_lock) or print STDERR "$prefix semop failed \n";
    shmread($SHMID, $Cache, 0, $SHMSIZE) or printf STDERR "$prefix shmread failed \n";
    substr($Cache, index($Cache, "\0")) = '';
    my $child_count_new = 1;
    if ($Cache =~ /^(\d+)$;(\d+)\n/) { # segment already exists (eg start of additional server)
        my $time_stamp   = $1;
        my $child_count  = $2;
        $child_count_new = $child_count + 1;
        $Cache =~ s/^$time_stamp$;$child_count\n/$time_stamp$;$child_count_new\n/;
    } else { # first child => initialize segment
        $Cache = time . "$;$child_count_new\n";
    }
    print STDERR "$prefix child count = $child_count_new \n" if $Apache::AuthDigestDBI::DEBUG > 1;
    shmwrite($SHMID, $Cache, 0, $SHMSIZE) or printf STDERR "$prefix shmwrite failed \n";
    semop($SEMID, $release_lock) or print STDERR "$prefix semop failed \n";
    1;
}


# The PerlChildExitHandler decrements the child count or destroys the shared memory 
# segment upon server shutdown, which is defined by the exit of the last child.
# Note: this handler runs in every child server, but not in the main server.

sub childexit {
    my $prefix = "$$ Apache::AuthDigestDBI         PerlChildExitHandler";
    # read Cache from shared memory, decrement child count and exit or write Cache to shared memory
    semop($SEMID, $obtain_lock) or print STDERR "$prefix semop failed \n";
    shmread($SHMID, $Cache, 0, $SHMSIZE) or printf STDERR "$prefix shmread failed \n";
    substr($Cache, index($Cache, "\0")) = '';
    $Cache =~ /^(\d+)$;(\d+)\n/;
    my $time_stamp  = $1;
    my $child_count = $2;
    my $child_count_new = $child_count - 1;
    if ($child_count_new) {
        print STDERR "$prefix child count = $child_count \n" if $Apache::AuthDigestDBI::DEBUG > 1;
        # write Cache into shared memory
        $Cache =~ s/^$time_stamp$;$child_count\n/$time_stamp$;$child_count_new\n/;
        shmwrite($SHMID, $Cache, 0, $SHMSIZE) or printf STDERR "$prefix shmwrite failed \n";
        semop($SEMID, $release_lock) or print STDERR "$prefix semop failed \n";
    } else { # last child
        # remove shared memory segment and semaphore set
        print STDERR "$prefix child count = $child_count, remove shared memory $SHMID and semaphore $SEMID \n" if $Apache::AuthDigestDBI::DEBUG > 1;
        shmctl($SHMID,    IPC_RMID, 0) or print STDERR "$prefix shmctl failed \n";
        semctl($SEMID, 0, IPC_RMID, 0) or print STDERR "$prefix semctl failed \n";
    }
    1;
}


# The PerlCleanupHandler skips through the cache and deletes any outdated entry.
# Note: this handler runs after the response has been sent to the client.

sub cleanup {
    my $prefix = "$$ Apache::AuthDigestDBI         PerlCleanupHandler";
    print STDERR "$prefix \n" if $Apache::AuthDigestDBI::DEBUG > 1;
    my $now = time;
    if ($SHMID) { # do we keep the cache in shared memory ?
        semop($SEMID, $obtain_lock) or print STDERR "$prefix semop failed \n";
        shmread($SHMID, $Cache, 0, $SHMSIZE) or printf STDERR "$prefix shmread failed \n";
        substr($Cache, index($Cache, "\0")) = ''; 
    }
    my $newCache = "$now$;"; # initialize timestamp for CleanupHandler
    my ($time_stamp, $child_count);
    foreach my $record (split(/\n/, $Cache)) {
        if (!$time_stamp) { # first record: timestamp of CleanupHandler and child count
            ($time_stamp, $child_count) = split(/$;/, $record);
            $newCache .= "$child_count\n";
            next;
        }
        my ($id, $last_access, $passwd, $groups) = split(/$;/, $record);
        my $diff = $now - $last_access;
        if ($diff >= $CacheTime) {
            print STDERR "$prefix delete >$id<, last access $diff s before \n" if $Apache::AuthDigestDBI::DEBUG > 1;
        } else {
            print STDERR "$prefix keep   >$id<, last access $diff s before \n" if $Apache::AuthDigestDBI::DEBUG > 1;
            $newCache .= "$id$;$now$;$passwd$;$groups\n";
        }
    }
    $Cache = $newCache;
    if ($SHMID) { # write Cache to shared memory
        shmwrite($SHMID, $Cache, 0, $SHMSIZE) or printf STDERR "$prefix shmwrite failed \n";
        semop($SEMID, $release_lock) or print STDERR "$prefix semop failed \n";
    }
    1;
}

sub old_decode_base64 ($)
{
	local($^W) = 0; # unpack("u",...) gives bogus warning in 5.00[123]
	
	my $str = shift;
	$str =~ tr|A-Za-z0-9+=/||cd;            # remove non-base64 chars
	if (length($str) % 4) {
		require Carp;
		Carp::carp("Length of base64 data not a multiple of 4")
	}
	$str =~ s/=+$//;                        # remove padding
	$str =~ tr|A-Za-z0-9+/| -_|;            # convert to uuencoded format
	
	return join '', map( unpack("u", chr(32 + length($_)*3/4) . $_),
					$str =~ /(.{1,60})/gs);
}

1;

__END__