merging 1.2 and bugfixes for auth and login
[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                 warn "passe 1";
130         # Get session ID from cookie.
131         my $sessionID=$query->cookie('sessionID');
132                 warn "sessionId = $sessionID";
133                 # FIXME - Error-checking: if the user isn't allowing cookies,
134                 # $sessionID will be undefined. Don't confuse this with an
135                 # expired cookie.
136
137         my $message='';
138
139         # Make sure the session ID is (still) good.
140         my $dbh = C4::Context->dbh;
141         my $sth=$dbh->prepare("select userid,ip,lasttime from sessions where sessionid=?");
142         $sth->execute($sessionID);
143         if ($sth->rows) {
144                 warn "IF 1";
145                 my ($userid, $ip, $lasttime) = $sth->fetchrow;
146                 # FIXME - Back door for tonnensen
147                 if ($lasttime<time()-45 && $userid ne 'tonnesen') {
148                 # This session has been inactive for >45 seconds, and
149                 # doesn't belong to user tonnensen. It has expired.
150                 $message="You have been logged out due to inactivity.";
151
152                 # Remove this session ID from the list of active sessions.
153                 # FIXME - Ought to have a cron job clean this up as well.
154                 my $sti=$dbh->prepare("delete from sessions where sessionID=?");
155                 $sti->execute($sessionID);
156
157                 # Add an entry to sessionqueries, so that we can restart
158                 # the script once the user has authenticated.
159                 my $scriptname=$ENV{'SCRIPT_NAME'};     # FIXME - Unused
160                 my $selfurl=$query->self_url();
161                 $sti=$dbh->prepare("insert into sessionqueries (sessionID, userid, value) values (?, ?, ?)");
162                 $sti->execute($sessionID, $userid, $selfurl);
163
164                 # Log the fact that someone tried to use an expired session ID.
165                 # FIXME - Ought to have a better logging mechanism,
166                 # ideally some wrapper that logs either to a
167                 # user-specified file, or to syslog, as determined by
168                 # either an entry in /etc/koha.conf, or a system
169                 # preference.
170                 open L, ">>/tmp/sessionlog";
171                 my $time=localtime(time());
172                 printf L "%20s from %16s logged out at %30s (inactivity).\n", $userid, $ip, $time;
173                 close L;
174                 } elsif ($ip ne $ENV{'REMOTE_ADDR'}) {
175                 warn "ELSE1";
176                 # This session is coming from an IP address other than the
177                 # one where it was set. The user might be doing something
178                 # naughty.
179                 my $newip=$ENV{'REMOTE_ADDR'};
180
181                 $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)";
182                 } else {
183                 warn "ELSE2";
184                 # This appears to be a valid session. Update the time
185                 # stamp on it and return.
186                 my $cookie=$query->cookie(-name => 'sessionID',
187                                                 -value => $sessionID,
188                                                 -expires => '+1y');
189                 my $sti=$dbh->prepare("update sessions set lasttime=? where sessionID=?");
190                 $sti->execute(time(), $sessionID);
191                 return ($userid, $cookie, $sessionID);
192                 }
193         }
194         warn "AFTER";
195         # If we get this far, it's because we haven't received a cookie
196         # with a valid session ID. Need to start a new session and set a
197         # new cookie.
198
199         if ($authnotrequired) {
200         warn "authnotrequired";
201                 # This script doesn't require the user to be logged in. Return
202                 # just the cookie, without user ID or session ID information.
203                 my $cookie=$query->cookie(-name => 'sessionID',
204                                         -value => '',
205                                         -expires => '+1y');
206                 return('', $cookie, '');
207         } else {
208                 warn "ELSE3";
209                 # This script requires authorization. Assume that we were
210                 # given user and password information; generate a new session.
211
212                 # Generate a new session ID.
213                 ($sessionID) || ($sessionID=int(rand()*100000).'-'.time());
214                 my $userid=$query->param('userid');
215                 my $password=$query->param('password');
216                 warn "calling checkpw";
217                 if (checkpw($dbh, $userid, $password)) {
218                         # The given password is valid
219                         warn "VALID";
220                         # Delete any old copies of this session.
221                         my $sti=$dbh->prepare("delete from sessions where sessionID=? and userid=?");
222                         $sti->execute($sessionID, $userid);
223
224                         # Add this new session to the 'sessions' table.
225                         $sti=$dbh->prepare("insert into sessions (sessionID, userid, ip,lasttime) values (?, ?, ?, ?)");
226                         $sti->execute($sessionID, $userid, $ENV{'REMOTE_ADDR'}, time());
227
228                         # See if there's an entry for this session ID and user in
229                         # the 'sessionqueries' table. If so, then use that entry
230                         # to generate an HTTP redirect that'll take the user to
231                         # where ve wanted to go in the first place.
232                         $sti=$dbh->prepare("select value from sessionqueries where sessionID=? and userid=?");
233                                         # FIXME - There is no sessionqueries.value
234                         $sti->execute($sessionID, $userid);
235                         if ($sti->rows) {
236                                 my $stj=$dbh->prepare("delete from sessionqueries where sessionID=?");
237                                 $stj->execute($sessionID);
238                                 my ($selfurl) = $sti->fetchrow;
239                                 print $query->redirect($selfurl);
240                                 exit;
241                         }
242                         open L, ">>/tmp/sessionlog";
243                         my $time=localtime(time());
244                         printf L "%20s from %16s logged in  at %30s.\n", $userid, $ENV{'REMOTE_ADDR'}, $time;
245                         close L;
246                         my $cookie=$query->cookie(-name => 'sessionID',
247                                                         -value => $sessionID,
248                                                         -expires => '+1y');
249                         return ($userid, $cookie, $sessionID);
250                 } else {
251                         # Either we weren't given a user id and password, or else
252                         # the password was invalid.
253                         warn "INVALID";
254                         if ($userid) {
255                                 $message="Invalid userid or password entered.";
256                         }
257                         my $parameters;
258                         foreach (param $query) {
259                                 $parameters->{$_}=$query->{$_};
260                         }
261                         my $cookie=$query->cookie(-name => 'sessionID',
262                                                         -value => $sessionID,
263                                                         -expires => '+1y');
264                         return ("",$cookie,$sessionID);
265                         print $query->header(-cookie=>$cookie);
266                         print qq|
267 <html>
268 <body background=/images/kohaback.jpg>
269 <center>
270 <h2>$message</h2>
271
272 <form method=post>
273 <table border=0 cellpadding=10 cellspacing=0 width=60%>
274     <tr><td align=center valign=top>
275
276     <table border=0 bgcolor=#dddddd cellpadding=10 cellspacing=0>
277     <tr><th colspan=2 background=/images/background-mem.gif><font size=+2>Koha Login</font></th></tr>
278     <tr><td>Name:</td><td><input name=userid></td></tr>
279     <tr><td>Password:</td><td><input type=password name=password></td></tr>
280     <tr><td colspan=2 align=center><input type=submit value=login></td></tr>
281     </table>
282
283     </td><td align=center valign=top>
284
285     <table border=0 bgcolor=#dddddd cellpadding=10 cellspacing=0>
286     <tr><th background=/images/background-mem.gif><font size=+2>Demo Information</font></th></tr>
287     <td>
288     Log in as librarian/koha or patron/koha.  The timeout is set to 40 seconds of
289     inactivity for the purposes of this demo.  You can navigate to the Circulation
290     or Acquisitions modules and you should see an indicator in the upper left of
291     the screen saying who you are logged in as.  If you want to try it out with
292     a longer timout period, log in as tonnesen/koha and there will be no
293     timeout period.
294     <p>
295     You can also log in using a patron cardnumber.   Try V10000008 and
296     V1000002X with password koha.
297     </td>
298     </tr>
299     </table>
300     </td></tr>
301 </table>
302 </form>
303 </body>
304 </html>
305 |;
306                 exit;
307                 }
308         }
309 }
310
311 # checkpw
312 # Takes a database handle, user ID, and password, and verifies that
313 # the password is good. The user ID may be either a user ID or a card
314 # number.
315 # Returns 1 if the password is good, or 0 otherwise.
316 sub checkpw {
317
318         # This should be modified to allow a select of authentication schemes (ie LDAP)
319         # as well as local authentication through the borrowers tables passwd field
320         #
321         my ($dbh, $userid, $password) = @_;
322         my $sth;
323
324         # Try the user ID.
325         $sth = $dbh->prepare("select password from borrowers where userid=?");
326         $sth->execute($userid);
327         if ($sth->rows) {
328                 my ($md5password) = $sth->fetchrow;
329                 if (md5_base64($password) eq $md5password) {
330                 return 1;               # The password matches
331                 }
332         }
333
334         # Try the card number.
335         $sth = $dbh->prepare("select password from borrowers where cardnumber=?");
336         $sth->execute($userid);
337         if ($sth->rows) {
338                 my ($md5password) = $sth->fetchrow;
339                 if (md5_base64($password) eq $md5password) {
340                 return 1;               # The password matches
341                 }
342         }
343         if ($userid eq C4::Context->config('user') && $password eq C4::Context->config('pass')) {
344                 # Koha superuser account
345                 return 2;
346         }
347         return 0;               # Either there's no such user, or the password
348                                 # doesn't match.
349 }
350
351
352 END { }       # module clean-up code here (global destructor)
353
354 1;
355 __END__
356
357 =back
358
359 =head1 SEE ALSO
360
361 CGI(3)
362
363 Digest::MD5(3)
364
365 =cut