File Coverage

blib/lib/CGI/Application/Plugin/Authentication/Driver/DBI.pm
Criterion Covered Total %
statement 70 71 98.5
branch 37 38 97.3
condition 5 5 100.0
subroutine 4 4 100.0
pod 1 1 100.0
total 117 119 98.3


line stmt bran cond sub pod time code
1             package CGI::Application::Plugin::Authentication::Driver::DBI;
2              
3 5     5   30 use strict;
  5         11  
  5         224  
4 5     5   31 use warnings;
  5         12  
  5         338  
5             our $VERSION = '0.20';
6              
7 5     5   147 use base qw(CGI::Application::Plugin::Authentication::Driver);
  5         12  
  5         4847  
8              
9             =head1 NAME
10              
11             CGI::Application::Plugin::Authentication::Driver::DBI - DBI Authentication driver
12              
13             =head1 VERSION
14              
15             This document describes CGI::Application::Plugin::Authentication::Driver::DBI version 0.20
16              
17             =head1 SYNOPSIS
18              
19             use base qw(CGI::Application);
20             use CGI::Application::Plugin::Authentication;
21              
22             __PACKAGE__->authen->config(
23             DRIVER => [ 'DBI',
24             DBH => $self->dbh,
25             TABLE => 'user',
26             CONSTRAINTS => {
27             'user.name' => '__CREDENTIAL_1__',
28             'MD5:user.password' => '__CREDENTIAL_2__'
29             },
30             ],
31             );
32              
33              
34             =head1 DESCRIPTION
35              
36             This Authentication driver uses the DBI module to allow you to authenticate against
37             any database for which there is a DBD module. You can either provide an active
38             database handle, or provide the parameters necessary to connect to the database.
39              
40             When describing the database structure, you need to specify some or all of the
41             following parameters: TABLE(S), JOIN_ON, COLUMNS, CONSTRAINTS, ORDER_BY and
42             LIMIT.
43              
44             =head2 DBH
45              
46             The DBI database handle to use. Defaults to C<$self->dbh()>, which is provided and configured
47             through L
48              
49             =head2 TABLE(S) (required)
50              
51             Provide either a single table name, or an array of table names. You can give the
52             table names aliases which can be referenced in later columns.
53              
54             TABLE => 'users',
55              
56             - or -
57              
58             TABLES => ['users U', 'domains D'],
59              
60              
61             =head2 JOIN_ON (conditionally required)
62              
63             If you have specified multiple tables, then you need to provide an SQL expression that
64             can be used to join those tables.
65              
66             JOIN_ON => 'user.domainid = domain.id',
67              
68             - or -
69              
70             JOIN_ON => 'U.domainid = D.id',
71              
72              
73             =head2 COLUMNS (optional)
74              
75             This is a hash of columns/values that should be pulled out of the database and validated
76             locally in perl. Most credentials can be checked right in the database (example
77             username = ?), but some parameters may need to be tested locally in perl, so they
78             must be listed in the COLUMNS option. One example of a value that needs to be tested
79             in perl is a crypted password. In order to test a crypted password, you need to
80             take the entered password, and crypt it with the salt of the already crypted password.
81             But until we actually see the password that is in the database, we will not know the
82             value of the salt that was used to encrypt the password. So we pull the value out
83             using COLUMNS, and the test will be performed automatically in perl.
84              
85             Any value that matches __CREDENTIAL_n__ (where n is a number) will be replaced with
86             the corresponding credential that was entered by the user. For an explanation of
87             what the credentials are and where they come from, see the section headed with
88             CREDENTIALS in L.
89              
90             COLUMNS => { 'crypt:password' => '__CREDENTIAL_2__' },
91              
92              
93             =head2 CONSTRAINTS (optional)
94              
95             You will most likely always have some constraints to use. These constraints
96             will be added to the WHERE clause of the SQL query, and will ideally reduce
97             the number of returned rows to one.
98              
99             Any value that matches __CREDENTIAL_n__ (where n is a number) will be replaced with
100             the corresponding credential that was entered by the user. For an explanation of
101             what the credentials are and where they come from, see the section headed with
102             CREDENTIALS in L.
103              
104             CONSTRAINTS => {
105             'users.email' => '__CREDENTIAL_1__',
106             'MD5:users.passphrase' => '__CREDENTIAL_2__',
107             'users.active' => 1,
108             }
109              
110              
111             =head2 ORDER_BY (optional)
112              
113             This option allows you to order the result set, in case the query returns
114             multiple rows.
115              
116             ORDER_BY => 'created DESC'
117              
118             Note: This option is only useful if you also specify the COLUMNS option.
119              
120             =head2 LIMIT (optional)
121              
122             In some situations your query may return multiple rows when you only want it to
123             return one. For example if you insert and date a new row instead of updating
124             the existing row when the details for an account change. In this case you want
125             the newest record from the result set, so it will be important to order the
126             result set and limit it to return only one row.
127              
128             LIMIT => 1
129              
130             Note: This option is only useful if you also specify the COLUMNS option.
131              
132             =head1 ENCODED PASSWORDS
133              
134             It is quite common to store passwords in a database in some form that makes them hard
135             (or virtually impossible) to guess. Most of the time one way encryption techniques
136             like Unix crypt or MD5 hashes are used to store the password securely (I would recommend
137             using MD5 or SHA1 over Unix crypt). If you look at the examples listed above, you can
138             see that you can mark your columns with an encoding type. Here is another example:
139              
140             CONSTRAINTS => {
141             username => '__CREDENTIAL_1__',
142             'MD5:password' => '__CREDENTIAL_2__',
143             }
144              
145             Here the password field is expected to be stored in the database in MD5 format. In order for the
146             MD5 check to work for all databases, the password will be encoded using perl, and then checked
147             against the value in the database. So in effect, the following will be done:
148              
149             $username = 'test';
150             $password = '123';
151             $encoded_password = 'ICy5YqxZB1uWSwcVLSNLcA';
152             $sth = $dbh->prepare('SELECT count(*) FROM users WHERE username = ? AND password = ?';
153             $sth->execute($username, $encoded_password);
154             # I we found a row, then the user credentials are valid and the user is logged in
155              
156             This is all automatically performed behind the scenes when you specify that a certain field
157             in the database is encoded.
158              
159             We have to handle this slightly different when working with Unix crypt. In order to crypt
160             a password, you need to provide the crypt function with a 2 character salt value. These are
161             usually just generated randomly, and when the value is crypted, the first two characters of
162             the resulting string will be the 2 salt characters. The problem comes into play when you want
163             to check a password against a crypted password. You need to know the salt in order to
164             properly test the password. But in our case, the crypted password is in the DB. This means we
165             can not generate the crypted test password before we run the query against the database.
166              
167             So instead we pull the value of the crypted password out of the database, and then perform the
168             tests after the query, instead of before. Here is an example:
169              
170             CONSTRAINTS => { 'username' => '__CREDENTIAL_1__' },
171             COLUMNS => { 'crypt:password' => '__CREDENTIAL_2__' },
172              
173             And here is what will happen behind the scenes:
174              
175             $username = 'test';
176             $password = '123';
177             $sth = $dbh->prepare('SELECT password FROM users WHERE username = ?';
178             $sth->execute($username);
179             ($encoded_password) = $sth->fetchrow_array;
180             if ($encoded_password eq crypt($password, $encoded_password)) {
181             # The credentials are valid and the user is logged in
182             }
183              
184             Again, this is all done automatically behind the scenes, but I've included it here to illustrate how
185             the queries are performed, and how the comparisons are handled. For more information
186             see the section labelled ENCODED PASSWORDS in the L
187             docs.
188              
189              
190              
191             =head1 EXAMPLE
192              
193             # using multiple tables
194             # Here we check three credentials (user, password and domain) across
195             # two separate tables.
196             __PACKAGE__->authen->config(
197             DRIVER => [ 'DBI',
198             # the handle comes from $self->dbh, via the "DBH" plugin.
199             TABLES => ['user', 'domain'],
200             JOIN_ON => 'user.domainid = domain.id',
201             CONSTRAINTS => {
202             'user.name' => '__CREDENTIAL_1__',
203             'user.password' => '__CREDENTIAL_2__',
204             'domain.name' => '__CREDENTIAL_3__'
205             }
206             ],
207             );
208              
209             - or -
210              
211             # using filtered fields
212             # Here the password column contains values that are encoded using unix crypt
213             # and since we need to know the salt in order to encrypt the password
214             # properly, we need to pull out the password, and check it locally
215             __PACKAGE__->authen->config(
216             DRIVER => [ 'DBI',
217             DBH => $dbh, # provide your own DBI handle
218             TABLE => 'user',
219             CONSTRAINTS => { 'user.name' => '__CREDENTIAL_1__' }
220             COLUMNS => { 'crypt:password' => '__CREDENTIAL_2__' },
221             ],
222             );
223              
224             - or -
225              
226             # extra constraints
227             # Here we only check users where the 'active' column is true
228             __PACKAGE__->authen->config(
229             DRIVER => [ 'DBI',
230             TABLE => 'user',
231             CONSTRAINTS => {
232             'user.name' => '__CREDENTIAL_1__',
233             'user.password' => '__CREDENTIAL_2__',
234             'user.active' => 't'
235             },
236             ],
237             );
238              
239             - or -
240              
241             # all of them combined
242             # Here the user is required to enter a username and password (which is
243             # crypted), and a daily code that changes every day (which is encoded using
244             # an MD5 hash hex format and stored in upper case).
245             __PACKAGE__->authen->config(
246             DRIVER => [ 'DBI',
247             TABLES => ['user U', 'dailycode D'],
248             JOIN_ON => 'U.userid = D.userid',
249             CONSTRAINTS => {
250             'U.name' => '__CREDENTIAL_1__',
251             'uc:md5_hex:D.code' => '__CREDENTIAL_3__',
252             'D.date' => 'now'
253             },
254             COLUMNS => {
255             'crypt:U.password' => '__CREDENTIAL_2__'
256             },
257             ],
258             );
259              
260              
261              
262             =head1 METHODS
263              
264             =head2 verify_credentials
265              
266             This method will test the provided credentials against the values found in the database,
267             according to the Driver configuration.
268              
269             =cut
270              
271             sub verify_credentials {
272 72     72 1 122 my $self = shift;
273 72         362 my @creds = @_;
274              
275             # verify that all the options are OK
276 72         8825 my @_options = $self->options;
277 72 100       425 die "The DBI driver requires a hash of options" if @_options % 2;
278 71         1129 my %options = @_options;
279              
280             # Get a database handle - either one that is given to us, or see if there
281             # is a ->dbh method in the CGIApp module (This is provided by the
282             # CGI::Application::Plugin::DBH module, so use it if it is there).
283 71         97 my $dbh;
284 71 100       248 if ( $options{DBH} ) {
    50          
285 70         376 $dbh = $options{DBH};
286             } elsif ( $self->authen->_cgiapp->can('dbh') ) {
287 0         0 $dbh = $self->authen->_cgiapp->dbh;
288             } else {
289 1         11 die "No DBH handle passed to the DBI Driver, and no dbh() method detected";
290             }
291              
292             # Grab the database table names (TABLE and TABLES are synonymous)
293 70   100     324 my $tables = $options{TABLES} || $options{TABLE};
294 70 100       561 die "No TABLE parameter defined" unless defined($tables);
295 69 100       276 $tables = [$tables] unless ref $tables eq 'ARRAY';
296              
297             # See if we need to order the result set
298 69 100       334 my $order_by = $options{ORDER_BY} ? ' ORDER BY '.$options{ORDER_BY} : '';
299              
300             # See if we need to limit the result set
301 69 100       242 my $limit = $options{LIMIT} ? ' LIMIT '.$options{LIMIT} : '';
302              
303             # Grab all the columns that we need to pull out. We also grab a list of
304             # columns that are stripped of any encoding information.
305             # If no columns are provided we just select count(*) for efficiency.
306 69         84 my @columns;
307             my @stripped_columns;
308 69 100       199 if ( $options{COLUMNS} ) {
309 38 100       1767 die "COLUMNS must be a hashref" unless ref $options{COLUMNS} eq 'HASH';
310 37         104 @columns = keys %{ $options{COLUMNS} };
  37         162  
311 37         196 @stripped_columns = $self->strip_field_names(@columns);
312             } else {
313 31         67 @columns = ('count(*)');
314 31         70 @stripped_columns = @columns;
315             }
316              
317             # Process the constraints.
318             # We need to check for values indicate they should be replaced by
319             # a credential (__CREDENTIAL_\d+__), and we need to filter any values
320             # that are configured to be filtered
321 68         106 my %constraints;
322 68 100       197 if ( $options{CONSTRAINTS} ) {
323 67 100       231 die "CONSTRAINTS must be a hashref" unless ref $options{CONSTRAINTS} eq 'HASH';
324 66         118 while ( my ( $column, $value ) = each %{ $options{CONSTRAINTS} } ) {
  191         832  
325 125 100       581 if ( $value =~ /^__CREDENTIAL_(\d+)__$/ ) {
326 115         3814 $value = $creds[ $1 - 1 ];
327             }
328 125         539 $value = $self->filter( $column, $value );
329 125         378 $column = $self->strip_field_names($column);
330 125         342 $constraints{$column} = $value;
331             }
332             }
333              
334             # If we have multiple tables, then we need a join constraint
335 67         136 my $join_on = $options{JOIN_ON};
336              
337             # Build the SQL statement
338 67         393 my $sql = 'SELECT ' . join( ', ', @stripped_columns ) . ' FROM ' . join( ', ', @$tables ) . ' WHERE ';
339 67         88 my @where;
340 67 100       165 push @where, $join_on if $join_on;
341 67         339 push @where, map { $_ . ' = ?' } keys %constraints;
  125         468  
342 67         206 $sql .= join( ' AND ', @where );
343 67         254 my @params = values %constraints;
344 67         116 $sql .= $order_by;
345 67         94 $sql .= $limit;
346              
347             # prepare and execute the SQL
348 67   100     1255 my $sth = $dbh->prepare_cached($sql) || die "Failed to prepare SQL statement: " . $dbh->errstr;
349 65 100       19251 $sth->execute(@params) or die $dbh->errstr;
350              
351             # Figure out what to do with the results
352 64 100       245 if ( $options{COLUMNS} ) {
353             # Since we pulled out some columns, we assume that these columns were not checked
354             # in the constraints section, and we test them here.
355             # It is possible that we could have multiple rows, so keep checking until we
356             # find a row where all comparisons are successful.
357 37         615 while ( my @array = $sth->fetchrow_array ) {
358 14         29 my $match = 1;
359 14         52 foreach my $index ( 0 .. $#columns ) {
360 16         48 my $value = $options{COLUMNS}->{ $columns[$index] };
361 16 100       101 if ( $value =~ /^__CREDENTIAL_(\d+)__$/ ) {
362 14         48 $value = $creds[ $1 - 1 ];
363             }
364 16 100       96 if ( !$self->check_filtered( $columns[$index], $value, $array[$index] ) ) {
365             # This test failed, so there is no sense checking the rest of the values
366             # in this row so we bail out early
367 6         14 $match = 0;
368 6         14 last;
369             }
370             }
371 14 100       120 if ($match) {
372             # we found a match so clean up and return the first credential
373 8         76 $sth->finish;
374 8         99 return $creds[0];
375             }
376             }
377             } else {
378             # Since we are not pulling specific columns we just check
379             # to see if we matched at least one row
380 27         532 my ($count) = $sth->fetchrow_array;
381 27         157 $sth->finish;
382 27 100       138 return $creds[0] if $count;
383             }
384 51         685 return;
385             }
386              
387             =head1 SEE ALSO
388              
389             L, L, perl(1)
390              
391              
392             =head1 LICENCE AND COPYRIGHT
393              
394             Copyright (c) 2005, SiteSuite. All rights reserved.
395              
396             This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
397              
398              
399             =head1 DISCLAIMER OF WARRANTY
400              
401             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION.
402              
403             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
404              
405             =cut
406              
407             1;