Parent Directory
|
Revision Log
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 |