[system] / branches / rel-2-1-a1 / webwork-modperl / lib / WeBWorK / Authen.pm Repository:
ViewVC logotype

View of /branches/rel-2-1-a1/webwork-modperl/lib/WeBWorK/Authen.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1777 - (download) (as text) (annotate)
Thu Feb 5 00:05:11 2004 UTC (9 years, 3 months ago) by sh002i
Original Path: trunk/webwork-modperl/lib/WeBWorK/Authen.pm
File size: 13766 byte(s)
Closes bugs #345 and #293. From bugzilla: "This should work now. The
docs for Apache::Cookie claim that the module will accept relative
expiration dates such as "+30D" and "+1H", but apparently, this causes
the expiration date to be set to "now", which Galeon apparently didn't
mind but caused Safari and MSIE to throw the cookie away."

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader: webwork-modperl/lib/WeBWorK/Authen.pm,v 1.28 2004/01/18 03:39:06 gage Exp $
    5 #
    6 # This program is free software; you can redistribute it and/or modify it under
    7 # the terms of either: (a) the GNU General Public License as published by the
    8 # Free Software Foundation; either version 2, or (at your option) any later
    9 # version, or (b) the "Artistic License" which comes with this package.
   10 #
   11 # This program is distributed in the hope that it will be useful, but WITHOUT
   12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   13 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
   14 # Artistic License for more details.
   15 ################################################################################
   16 
   17 package WeBWorK::Authen;
   18 
   19 =head1 NAME
   20 
   21 WeBWorK::Authen - Check user identity, manage session keys.
   22 
   23 =cut
   24 
   25 use strict;
   26 use warnings;
   27 use Apache::Cookie;
   28 use Date::Format;
   29 
   30 use constant COOKIE_LIFESPAN => 60*60*24*30; # 30 days
   31 
   32 sub new($$$) {
   33   my $invocant = shift;
   34   my $class = ref($invocant) || $invocant;
   35   my $self = {};
   36   ($self->{r}, $self->{ce}, $self->{db}) = @_;
   37   bless $self, $class;
   38   return $self;
   39 }
   40 
   41 sub checkPassword($$$) {
   42   my ($self, $userID, $possibleClearPassword) = @_;
   43   my $Password = $self->{db}->getPassword($userID); # checked
   44   return 0 unless defined $Password;
   45   my $possibleCryptPassword = crypt($possibleClearPassword, $Password->password());
   46   return $possibleCryptPassword eq $Password->password();
   47 }
   48 
   49 sub generateKey($$) {
   50   my ($self, $userID) = @_;
   51   my @chars = @{ $self->{ce}->{sessionKeyChars} };
   52   my $length = $self->{ce}->{sessionKeyLength};
   53   srand;
   54   my $key = join ("", @chars[map rand(@chars), 1 .. $length]);
   55   return WeBWorK::DB::Record::Key->new(user_id=>$userID, key=>$key, timestamp=>time);
   56 }
   57 
   58 sub checkKey($$$) {
   59   my ($self, $userID, $possibleKey) = @_;
   60   my $Key = $self->{db}->getKey($userID); # checked
   61   return 0 unless defined $Key;
   62   if (time <= $Key->timestamp()+$self->{ce}->{sessionKeyTimeout}) {
   63     if ($possibleKey eq $Key->key()) {
   64       # unexpired and matches -- update timestamp
   65       $Key->timestamp(time);
   66       $self->{db}->putKey($Key);
   67       return 1;
   68     } else {
   69       # unexpired but doesn't match -- leave timestamp alone
   70       # we do this to keep an attacker from keeping someone's session
   71       # alive. (yeah, we don't match IPs.)
   72       return 0;
   73     }
   74   } else {
   75     # expired -- delete key
   76     $self->{db}->deleteKey($userID);
   77     return 0;
   78   }
   79 }
   80 
   81 sub unexpiredKeyExists($$) {
   82   my ($self, $userID) = @_;
   83   my $Key = $self->{db}->getKey($userID); # checked
   84   return 0 unless defined $Key;
   85   if (time <= $Key->timestamp()+$self->{ce}->{sessionKeyTimeout}) {
   86     # unexpired, but leave timestamp alone
   87     return 1;
   88   } else {
   89     # expired -- delete key
   90     $self->{db}->deleteKey($userID);
   91     return 0;
   92   }
   93 }
   94 
   95 sub fetchCookie {
   96   my ($self, $user, $key) = @_;
   97   my $r = $self->{r};
   98   my $ce = $self->{ce};
   99   my $courseID = $ce->{courseName};
  100 
  101   my %cookies = Apache::Cookie->fetch;
  102   my $cookie = $cookies{"WeBWorKCourseAuthen.$courseID"};
  103 
  104   if ($cookie) {
  105     #warn __PACKAGE__, ": fetchCookie: found a cookie for this course: \"", $cookie->as_string, "\"\n";
  106     #warn __PACKAGE__, ": fetchCookie: cookie has this value: \"", $cookie->value, "\"\n";
  107     my ($userID, $key) = split "\t", $cookie->value;
  108     if (defined $userID and defined $key and $userID ne "" and $key ne "") {
  109       #warn __PACKAGE__, ": fetchCookie: looks good, returning userID=$userID key=$key\n";
  110       return $userID, $key;
  111     } else {
  112       #warn __PACKAGE__, ": fetchCookie: malformed cookie. returning empty strings.\n";
  113       return "", "";
  114     }
  115   } else {
  116     #warn __PACKAGE__, ": fetchCookie: found no cookie for this course. returning empty strings.\n";
  117     return "", "";
  118   }
  119 }
  120 
  121 sub sendCookie {
  122   my ($self, $userID, $key) = @_;
  123   my $r = $self->{r};
  124   my $ce = $self->{ce};
  125   my $courseID = $ce->{courseName};
  126 
  127   my $expires = time2str("%a, %d-%h-%Y %H:%M:%S %Z", time+COOKIE_LIFESPAN, "GMT");
  128   my $cookie = Apache::Cookie->new($r,
  129     -name    => "WeBWorKCourseAuthen.$courseID",
  130     -value   => "$userID\t$key",
  131     -expires => $expires,
  132     -domain  => $r->hostname,
  133     -path    => $ce->{webworkURLRoot},
  134     -secure  => 0,
  135   );
  136   my $cookieString = $cookie->as_string;
  137 
  138   #warn __PACKAGE__, ": sendCookie: about to add Set-Cookie header with this string: \"", $cookie->as_string, "\"\n";
  139   $r->headers_out->set("Set-Cookie" => $cookie->as_string);
  140 }
  141 
  142 sub killCookie {
  143   my ($self) = @_;
  144   my $r = $self->{r};
  145   my $ce = $self->{ce};
  146   my $courseID = $ce->{courseName};
  147 
  148   my $expires = time2str("%a, %d-%h-%Y %H:%M:%S %Z", time-60*60*24, "GMT");
  149   my $cookie = Apache::Cookie->new($r,
  150     -name => "WeBWorKCourseAuthen.$courseID",
  151     -value => "\t",
  152     -expires => $expires,
  153     -domain => $r->hostname,
  154     -path => $ce->{webworkURLRoot},
  155     -secure => 0,
  156   );
  157   my $cookieString = $cookie->as_string;
  158 
  159   #warn __PACKAGE__, ": killCookie: about to add Set-Cookie header with this string: \"", $cookie->as_string, "\"\n";
  160   $r->headers_out->set("Set-Cookie" => $cookie->as_string);
  161 }
  162 
  163 # verify will return 1 if the person is who they say the are. If the
  164 # verification failed because of of invalid authentication data, a note will be
  165 # written in the request explaining why it failed. If the request failed because
  166 # no authentication data was provided, however, no note will be written, as this
  167 # is expected to happen whenever someone types in a URL manually, and is not
  168 # considered an error condition.
  169 sub verify($) {
  170   my $self = shift;
  171   my $r = $self->{r};
  172   my $ce = $self->{ce};
  173   my $db = $self->{db};
  174 
  175   my $practiceUserPrefix = $ce->{practiceUserPrefix};
  176   my $debugPracticeUser = $ce->{debugPracticeUser};
  177 
  178   my $force_passwd_authen = $r->param('force_passwd_authen');
  179   my $login_practice_user = $r->param('login_practice_user');
  180   my $send_cookie = $r->param("send_cookie");
  181 
  182   my $error;
  183   my $failWithoutError = 0;
  184   my $credentialSource = "params";
  185 
  186   my $user = $r->param('user');
  187   my $passwd = $r->param('passwd');
  188   my $key = $r->param('key');
  189 
  190   my ($cookieUser, $cookieKey) = $self->fetchCookie;
  191   #warn __PACKAGE__, ": verify: cookieUser=$cookieUser cookieKey=$cookieKey\n";
  192 
  193   VERIFY: {
  194     # This block is here so we can "last" out of it when we've
  195     # decided whether we're going to succeed or fail.
  196 
  197     if ($login_practice_user) {
  198       # ignore everything else, find an unused practice user
  199       my $found = 0;
  200       foreach my $userID (sort grep m/^$practiceUserPrefix/, $db->listUsers) {
  201         if (not $self->unexpiredKeyExists($userID)) {
  202           my $Key = $self->generateKey($userID);
  203           $db->addKey($Key);
  204           $r->param("user", $userID);
  205           $r->param("key", $Key->key);
  206           $found = 1;
  207           last;
  208         }
  209       }
  210       unless ($found) {
  211         $error = "No practice users are available. Please try again in a few minutes.";
  212       }
  213       last VERIFY;
  214     }
  215 
  216     # no authentication data was given. this is OK.
  217     unless (defined $user or defined $passwd or defined $key) {
  218       # check to see if a cookie was sent by the browser. if so, use the
  219       # user and key from the cookie for authentication. note that the
  220       # cookie is only used if no credentials are sent as parameters.
  221       if ($cookieUser and $cookieKey) {
  222         $user = $cookieUser;
  223         $key = $cookieKey;
  224         $r->param("user", $user);
  225         $r->param("key", $key);
  226         $credentialSource = "cookie";
  227       } else {
  228         $failWithoutError = 1;
  229         last VERIFY;
  230       }
  231     }
  232 
  233     if (defined $user and $force_passwd_authen) {
  234       $failWithoutError = 1;
  235       last VERIFY;
  236     }
  237 
  238     # no user was supplied.  somebody's building their own GET
  239     unless ($user) {
  240       $error = "You must specify a username.";
  241       last VERIFY;
  242     }
  243     ########################################################
  244     # Make sure user is in the database
  245     ########################################################
  246 
  247     my $userRecord    =   $db->getUser($user);
  248     unless (defined $userRecord) { # checked
  249       $error = "There is no account for $user in this course.";
  250       last VERIFY;
  251     }
  252     ########################################################
  253     # Make sure the user's status is defined.
  254     ########################################################
  255     unless (defined $userRecord->status) {
  256       $userRecord-> status('C');
  257       #warn "Setting status for user $user to C.  It was previously undefined.";
  258     }
  259     unless ($userRecord->status eq 'C') {
  260       $error  = "The user $user has been dropped from this course. ";
  261       last VERIFY;
  262 
  263     }
  264     ########################################################
  265     # it's a practice user.
  266     ########################################################
  267     if ($practiceUserPrefix and $user =~ /^$practiceUserPrefix/) {
  268       # we're not interested in a practice user's password
  269       $r->param("passwd", "");
  270 
  271 
  272       # we've got a key.
  273       if ($key) {
  274         if ($self->checkKey($user, $key)) {
  275           # they key was valid.
  276           last VERIFY;
  277         } else {
  278           # the key was invalid.
  279           $error = "Your session has timed out due to inactivity. You must login again.";
  280           last VERIFY;
  281         }
  282       }
  283 
  284       # -- here we know that a key was not supplied. --
  285 
  286       # it's the debug user.
  287       if ($debugPracticeUser and $user eq $debugPracticeUser) {
  288         # clobber any existing session, valid or not.
  289         my $Key = $self->generateKey($user);
  290         eval { $db->deleteKey($user) };
  291         $db->addKey($Key);
  292         $r->param("key", $Key->key());
  293         last VERIFY;
  294       }
  295 
  296       # an unexpired key exists -- the account is in use.
  297       if ($self->unexpiredKeyExists($user)) {
  298         $error = "That practice account is in use.";
  299         last VERIFY;
  300       }
  301 
  302       # here we know the account is not in use, so we
  303       # generate a new  session key (unexpiredKeyExists
  304       # deleted any expired key) and succeed!
  305       my $Key = $self->generateKey($user);
  306       $db->addKey($Key);
  307       $r->param("key", $Key->key());
  308       last VERIFY;
  309     }
  310 
  311     # -- here we know it's a regular user. --
  312 
  313     #########################################################
  314     # Fail with error message if status is D or dropped
  315     #########################################################
  316     if ($db->getUser($user)->status eq 'D' or $db->getUser($user)->status eq 'DROPPED') {
  317       $error  = "The user $user has been dropped from this course. Please contact
  318       your instructor if this is an error.";
  319       last VERIFY;
  320 
  321     }
  322     # a key was supplied.
  323     if ($key) {
  324       # we're not interested in a user's password if they're
  325       # supplying a key
  326       $r->param("passwd", "");
  327 
  328       if ($self->checkKey($user, $key)) {
  329         # valid key, so succeed.
  330         last VERIFY;
  331       } else {
  332         # invalid key. the login page doesn't propogate the key,
  333         # so we know this is an expired session.
  334         $error = "Your session has timed out due to inactivity. You must login again.";
  335         last VERIFY;
  336       }
  337     }
  338 
  339     #########################################################
  340     # a password was supplied.
  341     #########################################################
  342     if ($passwd) {
  343 
  344       if ($self->checkPassword($user, $passwd)) {
  345         # valid password, so create a new session. (we don't want
  346         # to reuse an old one, duh.)
  347         my $Key = $self->generateKey($user);
  348         eval { $db->deleteKey($user) };
  349         $db->addKey($Key);
  350         $r->param("key", $Key->key());
  351         # also delete the password
  352         $r->param("passwd", "");
  353         last VERIFY;
  354       } else {
  355         # incorrect password. fail.
  356         $error = "Incorrect username or password.";
  357         last VERIFY;
  358       }
  359     }
  360 
  361     # neither a key or a password were supplied.
  362     $error = "You must enter a password."
  363   }
  364 
  365   if (defined $error) {
  366     # authentication failed, store the error message
  367     $r->notes("authen_error",$error);
  368 
  369     # if we got a cookie, it probably has incorrect information in it. so
  370     # we want to get rid of it
  371     if ($cookieUser or $cookieKey) {
  372       #warn "fail with error: killing cookie";
  373       $self->killCookie;
  374     }
  375 
  376     return 0;
  377   } elsif ($failWithoutError) {
  378     # authentication failed, but we don't have any error message to report
  379 
  380     # if we got a cookie, it probably has incorrect information in it. so
  381     # we want to get rid of it
  382     if ($cookieUser or $cookieKey) {
  383       #warn "fail without error: killing cookie";
  384       $self->killCookie;
  385     }
  386 
  387     return 0;
  388   } else {
  389     # autentication succeeded!
  390 
  391     # we send a cookie if any of these conditions are met:
  392     # (a) a cookie was used for authentication
  393     # (b) a cookie was sent but not used for authentication, and the
  394     #     credentials used for authentication were the same as those in
  395     #     the cookie
  396     # (c) the user asked to have a cookie sent and is not a guest user.
  397     my $usedCookie = ($credentialSource eq "cookie") || 0;
  398 
  399     my $unusedCookieMatched = (defined($key) and defined($cookieUser) and defined($cookieKey) and
  400                                 $user eq $cookieUser and $key eq $cookieKey) || 0;
  401     my $userRequestsCookie = ($send_cookie and not $login_practice_user) || 0;
  402     #warn "usedCookie=$usedCookie\n";
  403     #warn "unusedCookieMatched=$unusedCookieMatched\n";
  404     #warn "userRequestsCookie=$userRequestsCookie\n";
  405     if ($usedCookie or $unusedCookieMatched or $userRequestsCookie) {
  406       #warn "succeed: sending cookie";
  407       $self->sendCookie($r->param("user"), $r->param("key"));
  408     } elsif ($cookieUser or $cookieKey) {
  409       # otherwise, we don't want any bad cookies sticking around
  410       #warn "succeed: killing cookie";
  411       $self->killCookie;
  412     }
  413     return 1;
  414   }
  415 
  416   # Whatever you do, don't delete this!
  417   critical($r);
  418   # One time, I deleted it, and my mother broke her back, my cat died, and
  419   # the Pope got a tummy ache. When I replaced the line, I received eternal
  420   # salvation and a check for USD 500.
  421 }
  422 
  423 1;
  424 
  425 __END__
  426 
  427 =head1 AUTHOR
  428 
  429 Written by Dennis Lambe Jr., malsyned (at) math.rochester.edu, and Sam
  430 Hathaway, sh002i (at) math.rochester.edu.
  431 
  432 =cut

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9