Parent Directory
|
Revision Log
merging changes from patches Original message: Status was being checked for 'C' only and counting anything else as a dropped student. The check now uses the more abstract siteDefaults variable from global.conf and admits auditing students as well Also removed redundant second status check that could never be reached.
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.30 2004/03/15 20:17:35 sh002i 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{$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 334 $r->param("passwd", ""); 335 336 if ($self->checkKey($user, $key)) { 337 # valid key, so succeed. 338 last VERIFY; 339 } else { 340 # invalid key. the login page doesn't propogate the key, 341 # so we know this is an expired session. 342 $error = "Your session has timed out due to inactivity. You must login again."; 343 last VERIFY; 344 } 345 } 346 347 ######################################################### 348 # a password was supplied. 349 ######################################################### 350 if ($passwd) { 351 352 if ($self->checkPassword($user, $passwd)) { 353 # valid password, so create a new session. (we don't want 354 # to reuse an old one, duh.) 355 my $Key = $self->generateKey($user); 356 eval { $db->deleteKey($user) }; 357 $db->addKey($Key); 358 $r->param("key", $Key->key()); 359 # also delete the password 360 $r->param("passwd", ""); 361 last VERIFY; 362 } else { 363 # incorrect password. fail. 364 $error = "Incorrect username or password."; 365 last VERIFY; 366 } 367 } 368 369 # neither a key or a password were supplied. 370 $error = "You must enter a password." 371 } 372 373 if (defined $error) { 374 # authentication failed, store the error message 375 $r->notes("authen_error",$error); 376 377 # if we got a cookie, it probably has incorrect information in it. so 378 # we want to get rid of it 379 if ($cookieUser or $cookieKey) { 380 #warn "fail with error: killing cookie"; 381 $self->killCookie; 382 } 383 384 return 0; 385 } elsif ($failWithoutError) { 386 # authentication failed, but we don't have any error message to report 387 388 # if we got a cookie, it probably has incorrect information in it. so 389 # we want to get rid of it 390 if ($cookieUser or $cookieKey) { 391 #warn "fail without error: killing cookie"; 392 $self->killCookie; 393 } 394 395 return 0; 396 } else { 397 # autentication succeeded! 398 399 # we send a cookie if any of these conditions are met: 400 # (a) a cookie was used for authentication 401 # (b) a cookie was sent but not used for authentication, and the 402 # credentials used for authentication were the same as those in 403 # the cookie 404 # (c) the user asked to have a cookie sent and is not a guest user. 405 my $usedCookie = ($credentialSource eq "cookie") || 0; 406 407 my $unusedCookieMatched = (defined($key) and defined($cookieUser) and defined($cookieKey) and 408 $user eq $cookieUser and $key eq $cookieKey) || 0; 409 my $userRequestsCookie = ($send_cookie and not $login_practice_user) || 0; 410 #warn "usedCookie=$usedCookie\n"; 411 #warn "unusedCookieMatched=$unusedCookieMatched\n"; 412 #warn "userRequestsCookie=$userRequestsCookie\n"; 413 if ($usedCookie or $unusedCookieMatched or $userRequestsCookie) { 414 #warn "succeed: sending cookie"; 415 $self->sendCookie($r->param("user"), $r->param("key")); 416 } elsif ($cookieUser or $cookieKey) { 417 # otherwise, we don't want any bad cookies sticking around 418 #warn "succeed: killing cookie"; 419 $self->killCookie; 420 } 421 return 1; 422 } 423 424 # Whatever you do, don't delete this! 425 critical($r); 426 # One time, I deleted it, and my mother broke her back, my cat died, and 427 # the Pope got a tummy ache. When I replaced the line, I received eternal 428 # salvation and a check for USD 500. 429 } 430 431 1; 432 433 __END__ 434 435 =head1 AUTHOR 436 437 Written by Dennis Lambe Jr., malsyned (at) math.rochester.edu, and Sam 438 Hathaway, sh002i (at) math.rochester.edu. 439 440 =cut
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |