File Coverage

blib/lib/Apache2/AuthCookieDBI.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             #===============================================================================
2             #
3             # $Id: AuthCookieDBI.pm,v 1.60 2011/03/12 20:14:41 matisse Exp $
4             #
5             # Apache2::AuthCookieDBI
6             #
7             # An AuthCookie module backed by a DBI database.
8             #
9             # See end of this file for Copyright notices.
10             #
11             # Author: Jacob Davies
12             # Maintainer: Matisse Enzer (as of version 2.0)
13             #
14             #
15             # This library is free software; you can redistribute it and/or
16             # modify it under the terms of the GNU Lesser General Public
17             # License as published by the Free Software Foundation; either
18             # version 2.1 of the License, or (at your option) any later version.
19             #
20             # This library is distributed in the hope that it will be useful,
21             # but WITHOUT ANY WARRANTY; without even the implied warranty of
22             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
23             # Lesser General Public License for more details.
24             #
25             # You should have received a copy of the GNU Lesser General Public
26             # License along with this library; if not, write to the Free Software
27             # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
28             #
29             #===============================================================================
30              
31             package Apache2::AuthCookieDBI;
32              
33 2     2   2203 use strict;
  2         5  
  2         93  
34 2     2   14 use warnings;
  2         5  
  2         65  
35 2     2   65 use 5.010_000;
  2         7  
  2         123  
36             our $VERSION = '2.17';
37              
38 2     2   900 use Apache2::AuthCookie;
  2         16  
  2         63  
39 2     2   12 use base qw( Apache2::AuthCookie );
  2         4  
  2         199  
40              
41 2     2   480 use Apache2::RequestRec;
  2         1208  
  2         46  
42 2     2   831 use DBI;
  2         547  
  2         50  
43 2     2   884 use Apache2::Log;
  0            
  0            
44             use Apache2::Const -compile => qw( OK HTTP_FORBIDDEN SERVER_ERROR :log );
45             use Apache2::ServerUtil;
46             use Carp qw();
47             use Digest::MD5 qw( md5_hex );
48             use Date::Calc qw( Today_and_Now Add_Delta_DHMS );
49              
50             # Also uses Crypt::CBC if you're using encrypted cookies.
51             # Also uses Apache2::Session if you're using sessions.
52             use English qw(-no_match_vars);
53              
54             #===============================================================================
55             # FILE (LEXICAL) G L O B A L S
56             #===============================================================================
57              
58             my %CIPHERS = ();
59              
60             # Stores Cipher::CBC objects in $CIPHERS{ idea:AuthName },
61             # $CIPHERS{ des:AuthName } etc.
62              
63             use constant COLON_REGEX => qr/ : /mx;
64             use constant DATE_TIME_STRING_REGEX =>
65             qr/ \A \d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2} \z /mx;
66             use constant EMPTY_STRING => q{};
67             use constant HEX_STRING_REGEX => qr/ \A [0-9a-fA-F]+ \z /mx;
68             use constant HYPHEN_REGEX => qr/ - /mx;
69             use constant PERCENT_ENCODED_STRING_REGEX => qr/ \A [a-zA-Z0-9_\%]+ \z /mx;
70             use constant THIRTY_TWO_CHARACTER_HEX_STRING_REGEX =>
71             qr/ \A [0-9a-fA-F]{32} \z /mx;
72             use constant TRUE => 1;
73             use constant WHITESPACE_REGEX => qr/ \s+ /mx;
74             use constant LOG_TYPE_AUTH => 'authentication';
75             use constant LOG_TYPE_SYSTEM => 'system';
76             use constant LOG_TYPE_TIMEOUT => 'timeout';
77              
78             #===============================================================================
79             # P E R L D O C
80             #===============================================================================
81              
82             =head1 NAME
83              
84             Apache2::AuthCookieDBI - An AuthCookie module backed by a DBI database.
85              
86             =head1 VERSION
87              
88             This is version 2.14
89              
90             =head1 COMPATIBILITY
91              
92             Starting with version 2.03 the module is in the Apache2::* namespace,
93             L. For F versions
94             there is: L
95              
96             =head1 SYNOPSIS
97              
98             # In httpd.conf or .htaccess
99            
100             # Optional: Initiate a persistent database connection using Apache::DBI.
101             # See: http://search.cpan.org/dist/Apache-DBI/
102             # If you choose to use Apache::DBI then the following directive must come
103             # before all other modules using DBI - just uncomment the next line:
104             #PerlModule Apache::DBI
105            
106            
107             PerlModule Apache2::AuthCookieDBI
108             PerlSetVar WhatEverPath /
109             PerlSetVar WhatEverLoginScript /login.pl
110              
111             # Optional, to share tickets between servers.
112             PerlSetVar WhatEverDomain .domain.com
113            
114             # These must be set
115             PerlSetVar WhatEverDBI_DSN "DBI:mysql:database=test"
116             PerlSetVar WhatEverDBI_SecretKey "489e5eaad8b3208f9ad8792ef4afca73598ae666b0206a9c92ac877e73ce835c"
117              
118             # These are optional, the module sets sensible defaults.
119             PerlSetVar WhatEverDBI_User "nobody"
120             PerlSetVar WhatEverDBI_Password "password"
121             PerlSetVar WhatEverDBI_UsersTable "users"
122             PerlSetVar WhatEverDBI_UserField "user"
123             PerlSetVar WhatEverDBI_PasswordField "password"
124             PerlSetVar WhatEverDBI_UserActiveField "" # Default is skip this feature
125             PerlSetVar WhatEverDBI_CryptType "none"
126             PerlSetVar WhatEverDBI_GroupsTable "groups"
127             PerlSetVar WhatEverDBI_GroupField "grp"
128             PerlSetVar WhatEverDBI_GroupUserField "user"
129             PerlSetVar WhatEverDBI_EncryptionType "none"
130             PerlSetVar WhatEverDBI_SessionLifetime 00-24-00-00
131              
132             # Protected by AuthCookieDBI.
133            
134             AuthType Apache2::AuthCookieDBI
135             AuthName WhatEver
136             PerlAuthenHandler Apache2::AuthCookieDBI->authenticate
137             PerlAuthzHandler Apache2::AuthCookieDBI->authorize
138             require valid-user
139             # or you can require users:
140             require user jacob
141             # You can optionally require groups.
142             require group system
143            
144              
145             # Login location.
146            
147             AuthType Apache2::AuthCookieDBI
148             AuthName WhatEver
149             SetHandler perl-script
150             PerlHandler Apache2::AuthCookieDBI->login
151              
152             # If the directopry you are protecting is the DocumentRoot directory
153             # then uncomment the following directive:
154             #Satisfy any
155            
156              
157             =head1 DESCRIPTION
158              
159             This module is an authentication handler that uses the basic mechanism provided
160             by Apache2::AuthCookie with a DBI database for ticket-based protection. It
161             is based on two tokens being provided, a username and password, which can
162             be any strings (there are no illegal characters for either). The username is
163             used to set the remote user as if Basic Authentication was used.
164              
165             On an attempt to access a protected location without a valid cookie being
166             provided, the module prints an HTML login form (produced by a CGI or any
167             other handler; this can be a static file if you want to always send people
168             to the same entry page when they log in). This login form has fields for
169             username and password. On submitting it, the username and password are looked
170             up in the DBI database. The supplied password is checked against the password
171             in the database; the password in the database can be plaintext, or a crypt()
172             or md5_hex() checksum of the password. If this succeeds, the user is issued
173             a ticket. This ticket contains the username, an issue time, an expire time,
174             and an MD5 checksum of those and a secret key for the server. It can
175             optionally be encrypted before returning it to the client in the cookie;
176             encryption is only useful for preventing the client from seeing the expire
177             time. If you wish to protect passwords in transport, use an SSL-encrypted
178             connection. The ticket is given in a cookie that the browser stores.
179              
180             After a login the user is redirected to the location they originally wished
181             to view (or to a fixed page if the login "script" was really a static file).
182              
183             On this access and any subsequent attempt to access a protected document, the
184             browser returns the ticket to the server. The server unencrypts it if
185             encrypted tickets are enabled, then extracts the username, issue time, expire
186             time and checksum. A new checksum is calculated of the username, issue time,
187             expire time and the secret key again; if it agrees with the checksum that
188             the client supplied, we know that the data has not been tampered with. We
189             next check that the expire time has not passed. If not, the ticket is still
190             good, so we set the username.
191              
192             Authorization checks then check that any "require valid-user" or "require
193             user jacob" settings are passed. Finally, if a "require group foo" directive
194             was given, the module will look up the username in a groups database and
195             check that the user is a member of one of the groups listed. If all these
196             checks pass, the document requested is displayed.
197              
198             If a ticket has expired or is otherwise invalid it is cleared in the browser
199             and the login form is shown again.
200              
201             =cut
202              
203             #===============================================================================
204             # P R I V A T E F U N C T I O N S
205             #===============================================================================
206              
207             # Get the cipher from the cache, or create a new one if the
208             # cached cipher hasn't been created.
209             sub _get_cipher_for_type {
210             my ( $class, $dbi_encryption_type, $auth_name, $secret_key ) = @_;
211             my $lc_encryption_type = lc $dbi_encryption_type;
212             my $message;
213              
214             if ( exists $CIPHERS{"$lc_encryption_type:$auth_name"} ) {
215             return $CIPHERS{"$lc_encryption_type:$auth_name"};
216             }
217              
218             my %cipher_for_type = (
219             des => sub {
220             return $CIPHERS{"des:$auth_name"}
221             || Crypt::CBC->new( -key => $secret_key, -cipher => 'DES' );
222             },
223             idea => sub {
224             return $CIPHERS{"idea:$auth_name"}
225             || Crypt::CBC->new( -key => $secret_key, -cipher => 'IDEA' );
226             },
227             blowfish => sub {
228             return $CIPHERS{"blowfish:$auth_name"}
229             || Crypt::CBC->new(
230             -key => $secret_key,
231             -cipher => 'Blowfish'
232             );
233             },
234             blowfish_pp => sub {
235             return $CIPHERS{"blowfish_pp:$auth_name"}
236             || Crypt::CBC->new(
237             -key => $secret_key,
238             -cipher => 'Blowfish_PP'
239             );
240             },
241             );
242             my $code_ref = $cipher_for_type{$lc_encryption_type}
243             || Carp::confess("Unsupported encryption type: '$dbi_encryption_type'");
244             my $cbc_object = $code_ref->();
245              
246             # Cache the object. Caught bug where we were not, thanks to unit tests.
247             $CIPHERS{"$lc_encryption_type:$auth_name"} = $cbc_object;
248              
249             return $cbc_object;
250             }
251              
252             sub _encrypt_session_key {
253             my $class = shift;
254             my $session_key = shift;
255             my $secret_key = shift;
256             my $auth_name = shift;
257             my $dbi_encryption_type = lc shift;
258             my $message;
259              
260             if ( !defined $dbi_encryption_type ) {
261             Carp::confess('$dbi_encryption_type must be defined.');
262             }
263              
264             if ( $dbi_encryption_type eq 'none' ) {
265             return $session_key;
266             }
267              
268             my $cipher = $class->_get_cipher_for_type( $dbi_encryption_type, $auth_name,
269             $secret_key );
270             my $encrypted_key = $cipher->encrypt_hex($session_key);
271             return $encrypted_key;
272             }
273              
274             #-------------------------------------------------------------------------------
275             # _log_not_set -- Log that a particular authentication variable was not set.
276              
277             sub _log_not_set {
278             my ( $class, $r, $variable ) = @_;
279             my $auth_name = $r->auth_name;
280             my $message = "${class}\t$variable not set for auth realm $auth_name";
281             $class->logger( $r, Apache2::Const::LOG_ERR, $message, undef,
282             LOG_TYPE_SYSTEM, $r->uri );
283             return;
284             }
285              
286             #-------------------------------------------------------------------------------
287             # _dir_config_var -- Get a particular authentication variable.
288              
289             sub _dir_config_var {
290             my ( $class, $r, $variable ) = @_;
291             my $auth_name = $r->auth_name;
292             return $r->dir_config("$auth_name$variable");
293             }
294              
295             #-------------------------------------------------------------------------------
296             # _dbi_config_vars -- Gets the config variables from the dir_config and logs
297             # errors if required fields were not set, returns undef if any of the fields
298             # had errors or a hash of the values if they were all OK. Takes a request
299             # object.
300              
301             my %CONFIG_DEFAULT = (
302             DBI_DSN => undef,
303             DBI_SecretKey => undef,
304             DBI_User => undef,
305             DBI_Password => undef,
306             DBI_UsersTable => 'users',
307             DBI_UserField => 'user',
308             DBI_PasswordField => 'password',
309             DBI_UserActiveField => EMPTY_STRING, # Default is don't use this feature
310             DBI_CryptType => 'none',
311             DBI_GroupsTable => 'groups',
312             DBI_GroupField => 'grp',
313             DBI_GroupUserField => 'user',
314             DBI_EncryptionType => 'none',
315             DBI_SessionLifetime => '00-24-00-00',
316             DBI_sessionmodule => 'none',
317             );
318              
319             sub _dbi_config_vars {
320             my ( $class, $r ) = @_;
321              
322             my %c; # config variables hash
323             foreach my $variable ( keys %CONFIG_DEFAULT ) {
324             my $value_from_config = $class->_dir_config_var( $r, $variable );
325             $c{$variable}
326             = defined $value_from_config
327             ? $value_from_config
328             : $CONFIG_DEFAULT{$variable};
329             if ( !defined $c{$variable} ) {
330             $class->_log_not_set( $r, $variable );
331             }
332             }
333              
334             # If we used encryption we need to pull in Crypt::CBC.
335             if ( $c{'DBI_EncryptionType'} ne 'none' ) {
336             require Crypt::CBC;
337             }
338              
339             # Compile module for password encryption, if needed.
340             if ( $c{'DBI_CryptType'} =~ '^sha') {
341             require Digest::SHA;
342             }
343              
344             return %c;
345             }
346              
347             =head1 APACHE CONFIGURATION DIRECTIVES
348              
349             All configuration directives for this module are passed in PerlSetVars. These
350             PerlSetVars must begin with the AuthName that you are describing, so if your
351             AuthName is PrivateBankingSystem they will look like:
352              
353             PerlSetVar PrivateBankingSystemDBI_DSN "DBI:mysql:database=banking"
354              
355             See also L for the directives required for any kind
356             of Apache2::AuthCookie-based authentication system.
357              
358             In the following descriptions, replace "WhatEver" with your particular
359             AuthName. The available configuration directives are as follows:
360              
361             =over 4
362              
363             =item C
364              
365             Specifies the DSN for DBI for the database you wish to connect to retrieve
366             user information. This is required and has no default value.
367              
368             =item C
369              
370             Specifies the secret key for this auth scheme. This should be a long
371             random string. This should be secret; either make the httpd.conf file
372             only readable by root, or put the PerlSetVar in a file only readable by
373             root and include it.
374              
375             This is required and has no default value.
376             (NOTE: In AuthCookieDBI versions 1.22 and earlier the secret key either could be
377             set in the configuration file itself
378             or it could be place in a seperate file with the path configured with
379             C.
380              
381             As of version 2.0 you must use C and not
382             C.
383              
384             If you want to put the secret key in a separate file then you can create a
385             separate file that uses C and include that
386             file in your main Apache configuration using Apaches' C
387             directive. You might wish to make the file not
388             world-readable. Also, make sure that the Perl environment variables are
389             not publically available, for example via the /perl-status handler.)
390             See also L in this man page.
391              
392              
393             =item C
394              
395             The user to log into the database as. This is not required and
396             defaults to undef.
397              
398             =item C
399              
400             The password to use to access the database. This is not required
401             and defaults to undef.
402              
403             Make sure that the Perl environment variables are
404             not publically available, for example via the /perl-status handler since the
405             password could be exposed.
406              
407             =item C
408              
409             The table that user names and passwords are stored in. This is not
410             required and defaults to 'users'.
411              
412             =item C
413              
414             The field in the above table that has the user name. This is not
415             required and defaults to 'user'.
416              
417             =item C
418              
419             The field in the above table that has the password. This is not
420             required and defaults to 'password'.
421              
422             =item C
423              
424             The field in the users' table that has a value indicating if the users' account
425             is "active". This is optional and the default is to not use this field.
426             If used then users will fail authentication if the value in this field
427             is not a Perlish true value, so NULL, 0, and the empty string are all false
428             values. The I class method exposes this setting (and may be
429             overidden in a subclass.)
430              
431             =item C
432              
433             What kind of hashing is used on the password field in the database. This can
434             be 'none', 'crypt', 'md5', 'sha256', 'sha384', or 'sha512'.
435              
436             C will use Digest::MD5::md5hex() and C will use
437             Digest::SHA::sha{n}_hex().
438              
439             This is not required and defaults to 'none'.
440              
441             =item C
442              
443             The table that has the user / group information. This is not required and
444             defaults to 'groups'.
445              
446             =item C
447              
448             The field in the above table that has the group name. This is not required
449             and defaults to 'grp' (to prevent conflicts with the SQL reserved word 'group').
450              
451             =item C
452              
453             The field in the above table that has the user name. This is not required
454             and defaults to 'user'.
455              
456             =item C
457              
458             What kind of encryption to use to prevent the user from looking at the fields
459             in the ticket we give them. This is almost completely useless, so don't
460             switch it on unless you really know you need it. It does not provide any
461             protection of the password in transport; use SSL for that. It can be 'none',
462             'des', 'idea', 'blowfish', or 'blowfish_pp'.
463              
464             This is not required and defaults to 'none'.
465              
466             =item C
467              
468             How long tickets are good for after being issued. Note that presently
469             Apache2::AuthCookie does not set a client-side expire time, which means that
470             most clients will only keep the cookie until the user quits the browser.
471             However, if you wish to force people to log in again sooner than that, set
472             this value. This can be 'forever' or a life time specified as:
473              
474             DD-hh-mm-ss -- Days, hours, minute and seconds to live.
475              
476             This is not required and defaults to '00-24-00-00' or 24 hours.
477              
478             =item C
479              
480             Which Apache2::Session module to use for persistent sessions.
481             For example, a value could be "Apache2::Session::MySQL". The DSN will
482             be the same as used for authentication. The session created will be
483             stored in $r->pnotes( WhatEver ).
484              
485             If you use this, you should put:
486              
487             PerlModule Apache2::Session::MySQL
488              
489             (or whatever the name of your session module is) in your httpd.conf file,
490             so it is loaded.
491              
492             If you are using this directive, you can timeout a session on the server side
493             by deleting the user's session. Authentication will then fail for them.
494              
495             This is not required and defaults to none, meaning no session objects will
496             be created.
497              
498             =back
499              
500             =cut
501              
502             #-------------------------------------------------------------------------------
503             # _now_year_month_day_hour_minute_second -- Return a string with the time in
504             # this order separated by dashes.
505              
506             sub _now_year_month_day_hour_minute_second {
507             return sprintf '%04d-%02d-%02d-%02d-%02d-%02d', Today_and_Now;
508             }
509              
510             sub _check_password {
511             my ( $class, $password, $crypted_password, $crypt_type ) = @_;
512             return
513             if not $crypted_password
514             ; # https://rt.cpan.org/Public/Bug/Display.html?id=62470
515              
516             my %password_checker = (
517             'none' => sub { return $password eq $crypted_password; },
518             'crypt' => sub {
519             $class->_crypt_digest( $password, $crypted_password ) eq
520             $crypted_password;
521             },
522             'md5' => sub { return md5_hex($password) eq $crypted_password; },
523             'sha256' => sub {
524             return Digest::SHA::sha256_hex($password) eq $crypted_password;
525             },
526             'sha384' => sub {
527             return Digest::SHA::sha384_hex($password) eq $crypted_password;
528             },
529             'sha512' => sub {
530             return Digest::SHA::sha512_hex($password) eq $crypted_password;
531             },
532             );
533             return $password_checker{$crypt_type}->();
534             }
535              
536             sub _crypt_digest {
537             my ( $class, $plaintext, $encrypted ) = @_;
538             my $salt = substr $encrypted, 0, 2;
539             return crypt $plaintext, $salt;
540             }
541              
542             #-------------------------------------------------------------------------------
543             # _percent_encode -- Percent-encode (like URI encoding) any non-alphanumberics
544             # in the supplied string.
545              
546             sub _percent_encode {
547             my ($str) = @_;
548             my $not_a_word = qr/ ( \W ) /x;
549             $str =~ s/$not_a_word/ uc sprintf '%%%02x', ord $1 /xmeg;
550             return $str;
551             }
552              
553             #-------------------------------------------------------------------------------
554             # _percent_decode -- Percent-decode (like URI decoding) any %XX sequences in
555             # the supplied string.
556              
557             sub _percent_decode {
558             my ($str) = @_;
559             my $percent_hex_string_regex = qr/ %([0-9a-fA-F]{2}) /x;
560             $str =~ s/$percent_hex_string_regex/ pack( "c",hex( $1 ) ) /xmge;
561             return $str;
562             }
563              
564             #-------------------------------------------------------------------------------
565             # _dbi_connect -- Get a database handle.
566              
567             sub _dbi_connect {
568             my ( $class, $r ) = @_;
569             Carp::confess('Failed to pass Apache request object') if not $r;
570              
571             my ( $pkg, $file, $line, $sub ) = caller(1);
572             my $info_message = "${class}\t_dbi_connect called in $sub at line $line";
573             $class->logger( $r, Apache2::Const::LOG_INFO, $info_message, undef,
574             LOG_TYPE_SYSTEM, $r->uri );
575              
576             my %c = $class->_dbi_config_vars($r);
577              
578             my $auth_name = $r->auth_name;
579              
580             # get the crypted password from the users database for this user.
581             my $dbh = DBI->connect_cached( $c{'DBI_DSN'}, $c{'DBI_User'},
582             $c{'DBI_Password'} );
583             if ( defined $dbh ) {
584             my $info_message
585             = "${class}\tconnect to $c{'DBI_DSN'} for auth realm $auth_name";
586             $class->logger( $r, Apache2::Const::LOG_INFO, $info_message, undef,
587             LOG_TYPE_SYSTEM, $r->uri );
588             return $dbh;
589             }
590             else {
591              
592             my $error_message
593             = "${class}\tcouldn't connect to $c{'DBI_DSN'} for auth realm $auth_name";
594             $class->logger( $r, Apache2::Const::LOG_ERR, $error_message,
595             LOG_TYPE_SYSTEM, undef, $r->uri );
596             return;
597             }
598             }
599              
600             #-------------------------------------------------------------------------------
601             # _get_crypted_password -- Get the users' password from the database
602             sub _get_crypted_password {
603             my ( $class, $r, $user ) = @_;
604             my $dbh = $class->_dbi_connect($r) || return;
605             my %c = $class->_dbi_config_vars($r);
606             my $auth_name = $r->auth_name;
607              
608             if ( !$class->user_is_active( $r, $user ) ) {
609             my $message
610             = "${class}\tUser '$user' is not active for auth realm $auth_name.";
611             $class->logger( $r, Apache2::Const::LOG_NOTICE, $message, $user,
612             LOG_TYPE_AUTH, $r->uri );
613             return;
614             }
615              
616             my $crypted_password = EMPTY_STRING;
617              
618             my $sql_query = <<"SQL";
619             SELECT `$c{'DBI_PasswordField'}`
620             FROM `$c{'DBI_UsersTable'}`
621             WHERE `$c{'DBI_UserField'}` = ?
622             AND (`$c{'DBI_PasswordField'}` != ''
623             AND `$c{'DBI_PasswordField'}` IS NOT NULL)
624             SQL
625             my $sth = $dbh->prepare_cached($sql_query);
626             $sth->execute($user);
627             ($crypted_password) = $sth->fetchrow_array();
628             $sth->finish();
629              
630             if ( _is_empty($crypted_password) ) {
631             my $message
632             = "${class}\tCould not select password using SQL query '$sql_query'";
633             $class->logger( $r, Apache2::Const::LOG_ERR, $message, $user,
634             LOG_TYPE_AUTH, $r->uri );
635             return;
636             }
637             return $crypted_password;
638             }
639              
640             sub _get_new_session {
641             my $class = shift;
642             my $r = shift;
643             my $user = shift;
644             my $auth_name = shift;
645             my $session_module = shift;
646             my $extra_data = shift;
647              
648             my $dbh = $class->_dbi_connect($r);
649             my %session;
650             tie %session, $session_module, undef,
651             +{
652             Handle => $dbh,
653             LockHandle => $dbh,
654             };
655              
656             $session{'user'} = $user;
657             $session{'extra_data'} = $extra_data;
658             return \%session;
659             }
660              
661             # Takes a list and returns a list of the same size.
662             # Any element in the inputs that is defined is returned unchanged. Elements that
663             # were undef are returned as empty strings.
664             sub _defined_or_empty {
665             my @args = @_;
666             my @all_defined = ();
667             foreach my $arg (@args) {
668             if ( defined $arg ) {
669             push @all_defined, $arg;
670             }
671             else {
672             push @all_defined, EMPTY_STRING;
673             }
674             }
675             return @all_defined;
676             }
677              
678             sub _is_empty {
679             my $string = shift;
680             return TRUE if not defined $string;
681             return TRUE if $string eq EMPTY_STRING;
682             return;
683             }
684              
685             #===============================================================================
686             # P U B L I C F U N C T I O N S
687             #===============================================================================
688              
689             sub extra_session_info {
690             my ( $class, $r, $user, $password, @extra_data ) = @_;
691              
692             return EMPTY_STRING;
693             }
694              
695             sub authen_cred {
696             my ( $class, $r, $user, $password, @extra_data ) = @_;
697             my $auth_name = $r->auth_name;
698             ( $user, $password ) = _defined_or_empty( $user, $password );
699              
700             if ( !length $user ) {
701             my $message
702             = "${class}\tno username supplied for auth realm $auth_name";
703             $class->logger( $r, Apache2::Const::LOG_NOTICE, $message, $user,
704             LOG_TYPE_AUTH, $r->uri );
705             return;
706             }
707              
708             if ( !length $password ) {
709             my $message
710             = "${class}\tno password supplied for auth realm $auth_name";
711             $class->logger( $r, Apache2::Const::LOG_NOTICE, $message, $user,
712             LOG_TYPE_AUTH, $r->uri );
713             return;
714             }
715              
716             # get the configuration information.
717             my %c = $class->_dbi_config_vars($r);
718              
719             # get the crypted password from the users database for this user.
720             my $crypted_password = $class->_get_crypted_password( $r, $user, \%c );
721              
722             # now return unless the passwords match.
723             my $crypt_type = lc $c{'DBI_CryptType'};
724             if ( !$class->_check_password( $password, $crypted_password, $crypt_type ) )
725             {
726             my $message
727             = "${class}\tcrypt_type: '$crypt_type' - passwords didn't match for user '$user' for auth realm $auth_name";
728             $class->logger( $r, Apache2::Const::LOG_NOTICE, $message, $user,
729             LOG_TYPE_AUTH, $r->uri );
730             return;
731             }
732              
733             # Successful login
734             my $message = "${class}\tSuccessful login for $user";
735             $class->logger( $r, Apache2::Const::LOG_DEBUG, $message, $user,
736             LOG_TYPE_AUTH, $r->uri );
737              
738             # Create the expire time for the ticket.
739             my $expire_time = _get_expire_time( $c{'DBI_SessionLifetime'} );
740              
741             # Now we need to %-encode non-alphanumberics in the username so we
742             # can stick it in the cookie safely.
743             my $enc_user = _percent_encode($user);
744              
745             # If we are using sessions, we create a new session for this login.
746             my $session_id = EMPTY_STRING;
747             if ( $c{'DBI_sessionmodule'} ne 'none' ) {
748             my $session = $class->_get_new_session( $r, $user, $auth_name,
749             $c{'DBI_sessionmodule'}, \@extra_data );
750             $r->pnotes( $auth_name, $session );
751             $session_id = $session->{_session_id};
752             }
753              
754             # OK, now we stick the username and the current time and the expire
755             # time and the session id (if any) together to make the public part
756             # of the session key:
757             my $current_time = _now_year_month_day_hour_minute_second;
758             my $public_part = "$enc_user:$current_time:$expire_time:$session_id";
759             $public_part
760             .= $class->extra_session_info( $r, $user, $password, @extra_data );
761              
762             # Now we calculate the hash of this and the secret key and then
763             # calculate the hash of *that* and the secret key again.
764             my $secretkey = $c{'DBI_SecretKey'};
765             if ( !defined $secretkey ) {
766             my $message
767             = "${class}\tdidn't have the secret key for auth realm $auth_name";
768             $class->logger( $r, Apache2::Const::LOG_ERR, $message, $user,
769             LOG_TYPE_SYSTEM, $r->uri );
770             return;
771             }
772             my $hash = md5_hex( join q{:}, $secretkey,
773             md5_hex( join q{:}, $public_part, $secretkey ) );
774              
775             # Now we add this hash to the end of the public part.
776             my $session_key = "$public_part:$hash";
777              
778             # Now we encrypt this and return it.
779             my $encrypted_session_key
780             = $class->_encrypt_session_key( $session_key, $secretkey, $auth_name,
781             $c{'DBI_EncryptionType'} );
782             return $encrypted_session_key;
783             }
784              
785             #-------------------------------------------------------------------------------
786             # Take a session key and check that it is still valid; if so, return the user.
787              
788             sub authen_ses_key {
789             my ( $class, $r, $encrypted_session_key ) = @_;
790              
791             my $auth_name = $r->auth_name;
792              
793             # Get the configuration information.
794             my %c = $class->_dbi_config_vars($r);
795              
796             # Get the secret key.
797             my $secret_key = $c{'DBI_SecretKey'};
798             if ( !defined $secret_key ) {
799             my $message
800             = "${class}\tdidn't have the secret key from for auth realm $auth_name";
801             $class->logger( $r, Apache2::Const::LOG_ERR, $message, undef,
802             LOG_TYPE_SYSTEM, $r->uri );
803             return;
804             }
805              
806             my $session_key = $class->decrypt_session_key( $r, $c{'DBI_EncryptionType'},
807             $encrypted_session_key, $secret_key )
808             || return;
809              
810             # Break up the session key.
811             my ( $enc_user, $issue_time, $expire_time, $session_id, @rest )
812             = split COLON_REGEX, $session_key;
813             my $hashed_string = pop @rest;
814              
815             # Let's check that we got passed sensible values in the cookie.
816             ($enc_user) = _defined_or_empty($enc_user);
817             if ( $enc_user !~ PERCENT_ENCODED_STRING_REGEX ) {
818             my $message
819             = "${class}\tbad percent-encoded user '$enc_user' recovered from session ticket for auth_realm '$auth_name'";
820             $class->logger( $r, Apache2::Const::LOG_ERR, $message, undef,
821             LOG_TYPE_SYSTEM, $r->uri );
822             return;
823             }
824              
825             # decode the user
826             my $user = _percent_decode($enc_user);
827              
828             ($issue_time) = _defined_or_empty($issue_time);
829             if ( $issue_time !~ DATE_TIME_STRING_REGEX ) {
830             my $message
831             = "${class}\tbad issue time '$issue_time' recovered from ticket for user $user for auth_realm $auth_name";
832             $class->logger( $r, Apache2::Const::LOG_ERR, $message, $user,
833             LOG_TYPE_SYSTEM, $r->uri );
834             return;
835             }
836              
837             ($expire_time) = _defined_or_empty($expire_time);
838             if ( $expire_time !~ DATE_TIME_STRING_REGEX ) {
839             my $message
840             = "${class}\tbad expire time $expire_time recovered from ticket for user $user for auth_realm $auth_name";
841             $class->logger( $r, Apache2::Const::LOG_ERR, $message, $user,
842             LOG_TYPE_SYSTEM, $r->uri );
843             return;
844             }
845             if ( $hashed_string !~ THIRTY_TWO_CHARACTER_HEX_STRING_REGEX ) {
846             my $message
847             = "${class}\tbad encrypted session_key $hashed_string recovered from ticket for user $user for auth_realm $auth_name";
848             $class->logger( $r, Apache2::Const::LOG_ERR, $message, $user,
849             LOG_TYPE_SYSTEM, $r->uri );
850             return;
851             }
852              
853             # If we're using a session module, check that their session exist.
854             if ( $c{'DBI_sessionmodule'} ne 'none' ) {
855             my %session;
856             my $dbh = $class->_dbi_connect($r) || return;
857              
858             my $tie_result = eval {
859             tie %session, $c{'DBI_sessionmodule'}, $session_id,
860             +{
861             Handle => $dbh,
862             LockHandle => $dbh,
863             };
864             };
865             if ( ( !$tie_result ) || $EVAL_ERROR ) {
866             my $message
867             = "${class}\tfailed to tie session hash to '$c{'DBI_sessionmodule'}' using session id $session_id for user $user for auth_realm $auth_name, error was '$EVAL_ERROR'";
868             $class->logger( $r, Apache2::Const::LOG_ERR, $message, $user,
869             LOG_TYPE_SYSTEM, $r->uri );
870             return;
871             }
872              
873             # Update a timestamp at the top level to make sure we sync.
874             $session{timestamp} = _now_year_month_day_hour_minute_second;
875             $r->pnotes( $auth_name, \%session );
876             }
877              
878             # Calculate the hash of the user, issue time, expire_time and
879             # the secret key and the session_id and then the hash of that
880             # and the secret key again.
881             my $new_hash = md5_hex(
882             join q{:},
883             $secret_key,
884             md5_hex(
885             join q{:}, $enc_user, $issue_time, $expire_time,
886             $session_id, @rest, $secret_key
887             )
888             );
889              
890             # Compare it to the hash they gave us.
891             if ( $new_hash ne $hashed_string ) {
892             my $message
893             = "${class}\thash '$hashed_string' in cookie did not match calculated hash '$new_hash' of contents for user $user for auth realm $auth_name";
894             $class->logger( $r, Apache2::Const::LOG_ERR, $message, $user,
895             LOG_TYPE_TIMEOUT, $r->uri );
896             return;
897             }
898              
899             # Check that their session hasn't timed out.
900             if ( _now_year_month_day_hour_minute_second gt $expire_time ) {
901             my $message
902             = "${class}\texpire time $expire_time has passed for user $user for auth realm $auth_name";
903             $class->logger( $r, Apache2::Const::LOG_INFO, $message, $user,
904             LOG_TYPE_TIMEOUT, $r->uri );
905             return;
906             }
907              
908             # If we're being paranoid about timing-out long-lived sessions,
909             # check that the issue time + the current (server-set) session lifetime
910             # hasn't passed too (in case we issued long-lived session tickets
911             # in the past that we want to get rid of). *** TODO ***
912             # if ( lc $c{'DBI_AlwaysUseCurrentSessionLifetime'} eq 'on' ) {
913              
914             # They must be okay, so return the user.
915             return $user;
916             }
917              
918             sub decrypt_session_key {
919             my ( $class, $r, $encryptiontype, $encrypted_session_key, $secret_key )
920             = @_;
921              
922             if ( $encryptiontype eq 'none' ) {
923             return $encrypted_session_key;
924             }
925              
926             my $auth_name = $r->auth_name;
927              
928             my $session_key;
929              
930             # Check that this looks like an encrypted hex-encoded string.
931             if ( $encrypted_session_key !~ HEX_STRING_REGEX ) {
932             my $message
933             = "${class}\tencrypted session key '$encrypted_session_key' doesn't look like it's properly hex-encoded for auth realm $auth_name";
934             $class->logger( $r, Apache2::Const::LOG_ERR, $message, undef,
935             LOG_TYPE_SYSTEM, $r->uri );
936             return;
937             }
938              
939             my $cipher = $class->_get_cipher_for_type( $encryptiontype, $auth_name,
940             $secret_key );
941             if ( !$cipher ) {
942             my $message
943             = "${class}\tunknown encryption type '$encryptiontype' for auth realm $auth_name";
944             $class->logger( $r, Apache2::Const::LOG_ERR, $message, undef,
945             LOG_TYPE_SYSTEM, $r->uri );
946             return;
947             }
948             $session_key = $cipher->decrypt_hex($encrypted_session_key);
949             return $session_key;
950             }
951              
952             sub group {
953             my ( $class, $r, $groups ) = @_;
954             my @groups = split( WHITESPACE_REGEX, $groups );
955              
956             my $auth_name = $r->auth_name;
957              
958             # Get the configuration information.
959             my %c = $class->_dbi_config_vars($r);
960              
961             my $user = $r->user;
962              
963             # See if we have a row in the groups table for this user/group.
964             my $dbh = $class->_dbi_connect($r) || return Apache2::Const::SERVER_ERROR;
965              
966             # Now loop through all the groups to see if we're a member of any:
967             my $sth = $dbh->prepare_cached( <<"EOS" );
968             SELECT `$c{'DBI_GroupUserField'}`
969             FROM `$c{'DBI_GroupsTable'}`
970             WHERE `$c{'DBI_GroupField'}` = ?
971             AND `$c{'DBI_GroupUserField'}` = ?
972             EOS
973             foreach my $group (@groups) {
974             $sth->execute( $group, $user );
975             if ( $sth->fetchrow_array ) {
976             $sth->finish();
977              
978             # add the group to an ENV var that CGI programs can access:
979             $r->subprocess_env( 'AUTH_COOKIE_DBI_GROUP' => $group );
980             return Apache2::Const::OK;
981             }
982             }
983             $sth->finish();
984              
985             my $message
986             = "${class}\tuser $user was not a member of any of the required groups @groups for auth realm $auth_name";
987             $class->logger( $r, Apache2::Const::LOG_INFO, $message, $user,
988             LOG_TYPE_AUTH, $r->uri );
989             return Apache2::Const::HTTP_FORBIDDEN;
990             }
991              
992             sub user_is_active {
993             my ( $class, $r, $user ) = @_;
994             my %c = $class->_dbi_config_vars($r);
995             my $active_field_name = $c{'DBI_UserActiveField'};
996              
997             if ( !$active_field_name ) {
998             return TRUE; # Default is that users are active
999             }
1000              
1001             my $dbh = $class->_dbi_connect($r) || return;
1002             my $sql_query = <<"SQL";
1003             SELECT `$active_field_name`
1004             FROM `$c{'DBI_UsersTable'}`
1005             WHERE `$c{'DBI_UserField'}` = ?
1006             SQL
1007              
1008             my $sth = $dbh->prepare_cached($sql_query);
1009             $sth->execute($user);
1010             my ($user_active_setting) = $sth->fetchrow_array;
1011             $sth->finish();
1012              
1013             return $user_active_setting;
1014             }
1015              
1016             #-------------------------------------------------------------------------------
1017              
1018             sub _get_expire_time {
1019             my $session_lifetime = shift;
1020             $session_lifetime = lc $session_lifetime;
1021              
1022             my $expire_time = EMPTY_STRING;
1023              
1024             if ( $session_lifetime eq 'forever' ) {
1025             $expire_time = '9999-01-01-01-01-01';
1026              
1027             # expire time in a zillion years if it's forever.
1028             return $expire_time;
1029             }
1030              
1031             my ( $deltaday, $deltahour, $deltaminute, $deltasecond )
1032             = split HYPHEN_REGEX, $session_lifetime;
1033              
1034             # Figure out the expire time.
1035             $expire_time = sprintf(
1036             '%04d-%02d-%02d-%02d-%02d-%02d',
1037             Add_Delta_DHMS( Today_and_Now, $deltaday, $deltahour,
1038             $deltaminute, $deltasecond
1039             )
1040             );
1041             return $expire_time;
1042             }
1043              
1044             sub logger {
1045             my ( $class, $r, $log_level, $message, $user, $log_type, @extra_args ) = @_;
1046              
1047             # $log_level should be an Apache constant, e.g. Apache2::Const::LOG_NOTICE
1048              
1049             # Sub-classes should override this method if they want to implent their
1050             # own logging strategy.
1051             #
1052             my @log_args = ( $message, @extra_args );
1053              
1054             my %apache_log_method_for_level = (
1055             Apache2::Const::LOG_DEBUG => 'debug',
1056             Apache2::Const::LOG_INFO => 'info',
1057             Apache2::Const::LOG_NOTICE => 'notice',
1058             Apache2::Const::LOG_WARNING => 'warn',
1059             Apache2::Const::LOG_ERR => 'error',
1060             Apache2::Const::LOG_CRIT => 'crit',
1061             Apache2::Const::LOG_ALERT => 'alert',
1062             Apache2::Const::LOG_EMERG => 'emerg',
1063             );
1064             my $log_method = $apache_log_method_for_level{$log_level};
1065             if ( !$log_method ) {
1066             my ( $pkg, $file, $line, $sub ) = caller(1);
1067             $r->log_error(
1068             "Unknown log_level '$log_level' passed to logger() from $sub at line $line in $file "
1069             );
1070             $log_method = 'log_error';
1071             }
1072             $r->log->$log_method(@log_args);
1073             }
1074              
1075             1;
1076              
1077             __END__