[system] / branches / rel-2-3-dev / webwork2 / lib / WeBWorK / Authen.pm Repository:
ViewVC logotype

View of /branches/rel-2-3-dev/webwork2/lib/WeBWorK/Authen.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2843 - (download) (as text) (annotate)
Wed Sep 29 15:59:12 2004 UTC (8 years, 7 months ago) by toenail
Original Path: trunk/webwork2/lib/WeBWorK/Authen.pm
File size: 13636 byte(s)
Modified flow of verify() so that a user doesn't have to login twice
if they have a cookie with an expired key.

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9