Replaced expressions of the form "$x = $x <op> $y" with "$x <op>= $y".
[koha.git] / C4 / Auth.pm
1 package C4::Auth;
2
3
4 # Copyright 2000-2002 Katipo Communications
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it under the
9 # terms of the GNU General Public License as published by the Free Software
10 # Foundation; either version 2 of the License, or (at your option) any later
11 # version.
12 #
13 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
14 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License along with
18 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
19 # Suite 330, Boston, MA  02111-1307 USA
20
21 use strict;
22 use Digest::MD5 qw(md5_base64);
23
24
25 require Exporter;
26 use C4::Context;
27
28 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
29
30 # set the version for version checking
31 $VERSION = 0.01;
32
33 =head1 NAME
34
35 C4::Auth - Authenticates Koha users
36
37 =head1 SYNOPSIS
38
39   use CGI;
40   use C4::Auth;
41
42   $query = new CGI;
43   ($userid, $cookie, $sessionID) = &checkauth($query);
44
45 =head1 DESCRIPTION
46
47 This module provides authentication for Koha users.
48
49 =head1 FUNCTIONS
50
51 =over 2
52
53 =cut
54
55 @ISA = qw(Exporter);
56 @EXPORT = qw(
57              &checkauth
58 );
59
60 =item checkauth
61
62   ($userid, $cookie, $sessionID) = &checkauth($query, $noauth);
63
64 Verifies that the user is authorized to run this script. Note that
65 C<&checkauth> will return if and only if the user is authorized, so it
66 should be called early on, before any unfinished operations (i.e., if
67 you've opened a file, then C<&checkauth> won't close it for you).
68
69 C<$query> is the CGI object for the script calling C<&checkauth>.
70
71 The C<$noauth> argument is optional. If it is set, then no
72 authorization is required for the script.
73
74 C<&checkauth> fetches user and session information from C<$query> and
75 ensures that the user is authorized to run scripts that require
76 authorization.
77
78 If C<$query> does not have a valid session ID associated with it
79 (i.e., the user has not logged in) or if the session has expired,
80 C<&checkauth> presents the user with a login page (from the point of
81 view of the original script, C<&checkauth> does not return). Once the
82 user has authenticated, C<&checkauth> restarts the original script
83 (this time, C<&checkauth> returns).
84
85 C<&checkauth> returns a user ID, a cookie, and a session ID. The
86 cookie should be sent back to the browser; it verifies that the user
87 has authenticated.
88
89 =cut
90 #'
91 # FIXME - (Or rather, proofreadme)
92 # As I understand it, the 'sessionqueries' table in the Koha database
93 # is supposed to save state while the user authenticates. If
94 # (re-)authentication is required, &checkauth saves the browser's
95 # original call to a new entry in sessionqueries, then presents a form
96 # for the user to authenticate. Once the user has authenticated
97 # visself, &checkauth retrieves the stored information from
98 # sessionqueries and allows the original request to proceed.
99 #
100 # One problem, however, is that sessionqueries only stores the URL,
101 # not the various values passed along from an HTML form. Thus, if the
102 # request came from a form and contains information on stuff to change
103 # (e.g., modify the contents of a virtual bookshelf), but the session
104 # has timed out, then when &checkauth finally allows the request to
105 # proceed, it will not contain the user's modifications. This is bad.
106 #
107 # Another problem is that entries in sessionqueries are supposed to be
108 # temporary, but there's no mechanism for removing them in case of
109 # error (e.g., the user can't remember vis password and walks away, or
110 # if the user's machine crashes in the middle of authentication).
111 #
112 # Perhaps a better implementation would be to use $query->param to get
113 # the parameter with which the original script was invoked, and pass
114 # that along through all of the authentication pages. That way, all of
115 # the pertinent information would be preserved, and the sessionqueries
116 # table could be removed.
117
118 sub checkauth {
119         my $query=shift;
120         # $authnotrequired will be set for scripts which will run without authentication
121         my $authnotrequired=shift;
122         if (my $userid=$ENV{'REMOTE_USERNAME'}) {
123                 # Using Basic Authentication, no cookies required
124                 my $cookie=$query->cookie(-name => 'sessionID',
125                                         -value => '',
126                                         -expires => '+1y');
127                 return ($userid, $cookie, '');
128         }
129         # Get session ID from cookie.
130         my $sessionID=$query->cookie('sessionID');
131                 # FIXME - Error-checking: if the user isn't allowing cookies,
132                 # $sessionID will be undefined. Don't confuse this with an
133                 # expired cookie.
134
135         my $message='';
136
137         # Make sure the session ID is (still) good.
138         my $dbh = C4::Context->dbh;
139         my $sth=$dbh->prepare("select userid,ip,lasttime from sessions where sessionid=?");
140         $sth->execute($sessionID);
141         if ($sth->rows) {
142                 my ($userid, $ip, $lasttime) = $sth->fetchrow;
143                 # FIXME - Back door for tonnensen
144                 if ($lasttime<time()-45 && $userid ne 'tonnesen') {
145                 # This session has been inactive for >45 seconds, and
146                 # doesn't belong to user tonnensen. It has expired.
147                 $message="You have been logged out due to inactivity.";
148
149                 # Remove this session ID from the list of active sessions.
150                 # FIXME - Ought to have a cron job clean this up as well.
151                 my $sti=$dbh->prepare("delete from sessions where sessionID=?");
152                 $sti->execute($sessionID);
153
154                 # Add an entry to sessionqueries, so that we can restart
155                 # the script once the user has authenticated.
156                 my $scriptname=$ENV{'SCRIPT_NAME'};     # FIXME - Unused
157                 my $selfurl=$query->self_url();
158                 $sti=$dbh->prepare("insert into sessionqueries (sessionID, userid, value) values (?, ?, ?)");
159                 $sti->execute($sessionID, $userid, $selfurl);
160
161                 # Log the fact that someone tried to use an expired session ID.
162                 # FIXME - Ought to have a better logging mechanism,
163                 # ideally some wrapper that logs either to a
164                 # user-specified file, or to syslog, as determined by
165                 # either an entry in /etc/koha.conf, or a system
166                 # preference.
167                 open L, ">>/tmp/sessionlog";
168                 my $time=localtime(time());
169                 printf L "%20s from %16s logged out at %30s (inactivity).\n", $userid, $ip, $time;
170                 close L;
171                 } elsif ($ip ne $ENV{'REMOTE_ADDR'}) {
172                 # This session is coming from an IP address other than the
173                 # one where it was set. The user might be doing something
174                 # naughty.
175                 my $newip=$ENV{'REMOTE_ADDR'};
176
177                 $message="ERROR ERROR ERROR ERROR<br>Attempt to re-use a cookie from a different ip address.<br>(authenticated from $ip, this request from $newip)";
178                 } else {
179                 # This appears to be a valid session. Update the time
180                 # stamp on it and return.
181                 my $cookie=$query->cookie(-name => 'sessionID',
182                                                 -value => $sessionID,
183                                                 -expires => '+1y');
184                 my $sti=$dbh->prepare("update sessions set lasttime=? where sessionID=?");
185                 $sti->execute(time(), $sessionID);
186                 return ($userid, $cookie, $sessionID);
187                 }
188         }
189         # If we get this far, it's because we haven't received a cookie
190         # with a valid session ID. Need to start a new session and set a
191         # new cookie.
192
193         my $insecure = C4::Context->preference("insecure");
194
195         if ($authnotrequired ||
196             (defined($insecure) && $insecure eq "yes")) {
197                 # This script doesn't require the user to be logged in. Return
198                 # just the cookie, without user ID or session ID information.
199                 my $cookie=$query->cookie(-name => 'sessionID',
200                                         -value => '',
201                                         -expires => '+1y');
202                 return('', $cookie, '');
203         } else {
204                 # This script requires authorization. Assume that we were
205                 # given user and password information; generate a new session.
206
207                 # Generate a new session ID.
208                 ($sessionID) || ($sessionID=int(rand()*100000).'-'.time());
209                 my $userid=$query->param('userid');
210                 my $password=$query->param('password');
211                 if (checkpw($dbh, $userid, $password)) {
212                         # The given password is valid
213                         # Delete any old copies of this session.
214                         my $sti=$dbh->prepare("delete from sessions where sessionID=? and userid=?");
215                         $sti->execute($sessionID, $userid);
216
217                         # Add this new session to the 'sessions' table.
218                         $sti=$dbh->prepare("insert into sessions (sessionID, userid, ip,lasttime) values (?, ?, ?, ?)");
219                         $sti->execute($sessionID, $userid, $ENV{'REMOTE_ADDR'}, time());
220
221                         # See if there's an entry for this session ID and user in
222                         # the 'sessionqueries' table. If so, then use that entry
223                         # to generate an HTTP redirect that'll take the user to
224                         # where ve wanted to go in the first place.
225                         $sti=$dbh->prepare("select value from sessionqueries where sessionID=? and userid=?");
226                                         # FIXME - There is no sessionqueries.value
227                         $sti->execute($sessionID, $userid);
228                         if ($sti->rows) {
229                                 my $stj=$dbh->prepare("delete from sessionqueries where sessionID=?");
230                                 $stj->execute($sessionID);
231                                 my ($selfurl) = $sti->fetchrow;
232                                 print $query->redirect($selfurl);
233                                 exit;
234                         }
235                         open L, ">>/tmp/sessionlog";
236                         my $time=localtime(time());
237                         printf L "%20s from %16s logged in  at %30s.\n", $userid, $ENV{'REMOTE_ADDR'}, $time;
238                         close L;
239                         my $cookie=$query->cookie(-name => 'sessionID',
240                                                         -value => $sessionID,
241                                                         -expires => '+1y');
242                         return ($userid, $cookie, $sessionID);
243                 } else {
244                         # Either we weren't given a user id and password, or else
245                         # the password was invalid.
246                         if ($userid) {
247                                 $message="Invalid userid or password entered.";
248                         }
249                         my $parameters;
250                         foreach (param $query) {
251                                 $parameters->{$_}=$query->{$_};
252                         }
253                         my $cookie=$query->cookie(-name => 'sessionID',
254                                                         -value => $sessionID,
255                                                         -expires => '+1y');
256                         return ("",$cookie,$sessionID);
257                 }
258         }
259 }
260
261 # checkpw
262 # Takes a database handle, user ID, and password, and verifies that
263 # the password is good. The user ID may be either a user ID or a card
264 # number.
265 # Returns 1 if the password is good, or 0 otherwise.
266 sub checkpw {
267
268         # This should be modified to allow a select of authentication schemes (ie LDAP)
269         # as well as local authentication through the borrowers tables passwd field
270         #
271         my ($dbh, $userid, $password) = @_;
272         my $sth;
273
274         # Try the user ID.
275         $sth = $dbh->prepare("select password from borrowers where userid=?");
276         $sth->execute($userid);
277         if ($sth->rows) {
278                 my ($md5password) = $sth->fetchrow;
279                 if (md5_base64($password) eq $md5password) {
280                 return 1;               # The password matches
281                 }
282         }
283
284         # Try the card number.
285         $sth = $dbh->prepare("select password from borrowers where cardnumber=?");
286         $sth->execute($userid);
287         if ($sth->rows) {
288                 my ($md5password) = $sth->fetchrow;
289                 if (md5_base64($password) eq $md5password) {
290                 return 1;               # The password matches
291                 }
292         }
293         if ($userid eq C4::Context->config('user') && $password eq C4::Context->config('pass')) {
294                 # Koha superuser account
295                 return 2;
296         }
297         return 0;               # Either there's no such user, or the password
298                                 # doesn't match.
299 }
300
301
302 END { }       # module clean-up code here (global destructor)
303
304 1;
305 __END__
306
307 =back
308
309 =head1 SEE ALSO
310
311 CGI(3)
312
313 Digest::MD5(3)
314
315 =cut