1package Apache2::AuthCookie::Base; 2$Apache2::AuthCookie::Base::VERSION = '3.30'; 3# ABSTRACT: Common Methods Shared by Apache2 and Apache2_4 AuthCookie Subclasses. 4 5use strict; 6use mod_perl2 '1.99022'; 7use Carp; 8 9use Apache::AuthCookie::Util qw(is_blank is_local_destination); 10use Apache2::AuthCookie::Params; 11use Apache2::RequestRec; 12use Apache2::RequestUtil; 13use Apache2::Log; 14use Apache2::Access; 15use Apache2::Response; 16use Apache2::URI; 17use Apache2::Util; 18use APR::Table; 19use Apache2::Const qw(OK DECLINED SERVER_ERROR M_GET HTTP_FORBIDDEN HTTP_MOVED_TEMPORARILY HTTP_OK); 20use Encode (); 21 22 23sub authenticate { 24 my ($auth_type, $r) = @_; 25 26 my $debug = $r->dir_config("AuthCookieDebug") || 0; 27 28 $r->server->log_error("authenticate() entry") if ($debug >= 3); 29 $r->server->log_error("auth_type " . $auth_type) if ($debug >= 3); 30 31 if (my $prev = ($r->prev || $r->main)) { 32 # we are in a subrequest or internal redirect. Just copy user from the 33 # previous or main request if its is present 34 if (defined $prev->user) { 35 $r->server->log_error('authenticate() is in a subrequest or internal redirect.') if $debug >= 3; 36 # encoding would have been handled in prev req, so do not encode here. 37 $r->user( $prev->user ); 38 return OK; 39 } 40 } 41 42 if ($debug >= 3) { 43 $r->server->log_error("r=$r authtype=". $r->auth_type); 44 } 45 46 if ($r->auth_type ne $auth_type) { 47 # This location requires authentication because we are being called, 48 # but we don't handle this AuthType. 49 $r->server->log_error("AuthType mismatch: $auth_type =/= ".$r->auth_type) if $debug >= 3; 50 return DECLINED; 51 } 52 53 # Ok, the AuthType is $auth_type which we handle, what's the authentication 54 # realm's name? 55 my $auth_name = $r->auth_name; 56 $r->server->log_error("auth_name $auth_name") if $debug >= 2; 57 unless ($auth_name) { 58 $r->server->log_error("AuthName not set, AuthType=$auth_type", $r->uri); 59 return SERVER_ERROR; 60 } 61 62 # Get the Cookie header. If there is a session key for this realm, strip 63 # off everything but the value of the cookie. 64 my $ses_key_cookie = $auth_type->key($r) || ''; 65 66 $r->server->log_error("ses_key_cookie " . $ses_key_cookie) if $debug >= 1; 67 $r->server->log_error("uri " . $r->uri) if $debug >= 2; 68 69 if ($ses_key_cookie) { 70 my ($auth_user, @args) = $auth_type->authen_ses_key($r, $ses_key_cookie); 71 72 if (!is_blank($auth_user) and scalar @args == 0) { 73 # We have a valid session key, so we return with an OK value. 74 # Tell the rest of Apache what the authentication method and 75 # user is. 76 77 $r->ap_auth_type($auth_type); 78 $r->user( $auth_type->_encode($r, $auth_user) ); 79 $r->server->log_error("user authenticated as $auth_user") 80 if $debug >= 1; 81 82 # send new cookie if SessionTimeout is on 83 if (my $expires = $r->dir_config("${auth_name}SessionTimeout")) { 84 $auth_type->send_cookie($r, $ses_key_cookie, 85 {expires => $expires}); 86 } 87 88 return OK; 89 } 90 elsif (scalar @args > 0 and $auth_type->can('custom_errors')) { 91 return $auth_type->custom_errors($r, $auth_user, @args); 92 } 93 else { 94 # There was a session key set, but it's invalid for some reason. So, 95 # remove it from the client now so when the credential data is posted 96 # we act just like it's a new session starting. 97 $auth_type->remove_cookie($r); 98 $r->subprocess_env('AuthCookieReason', 'bad_cookie'); 99 } 100 } 101 else { 102 $r->subprocess_env('AuthCookieReason', 'no_cookie'); 103 } 104 105 # This request is not authenticated, but tried to get a protected 106 # document. Send client the authen form. 107 return $auth_type->login_form($r); 108} 109 110 111sub cookie_name { 112 my ($self, $r) = @_; 113 114 my $auth_type = $r->auth_type; 115 my $auth_name = $r->auth_name; 116 117 my $cookie_name = $r->dir_config("${auth_name}CookieName") || 118 "${auth_type}_${auth_name}"; 119 120 return $cookie_name; 121} 122 123 124sub cookie_string { 125 my $self = shift; 126 my %p = @_; 127 for (qw/request key/) { 128 croak "missing required parameter $_" unless defined $p{$_}; 129 } 130 # its okay if value is undef here. 131 132 my $r = $p{request}; 133 134 $p{value} = '' unless defined $p{value}; 135 136 my $string = sprintf '%s=%s', @p{'key','value'}; 137 138 my $auth_name = $r->auth_name; 139 140 if (my $expires = $p{expires} || $r->dir_config("${auth_name}Expires")) { 141 $expires = Apache::AuthCookie::Util::expires($expires); 142 $string .= "; expires=$expires"; 143 } 144 145 $string .= '; path=' . ( $self->get_cookie_path($r) || '/' ); 146 147 if (my $domain = $r->dir_config("${auth_name}Domain")) { 148 $string .= "; domain=$domain"; 149 } 150 151 if ($r->dir_config("${auth_name}Secure")) { 152 $string .= '; secure'; 153 } 154 155 # HttpOnly is an MS extension. See 156 # http://msdn.microsoft.com/workshop/author/dhtml/httponly_cookies.asp 157 if ($r->dir_config("${auth_name}HttpOnly")) { 158 $string .= '; HttpOnly'; 159 } 160 161 # SameSite is an anti-CSRF cookie property. See 162 # https://www.owasp.org/index.php/SameSite 163 if (my $samesite = $r->dir_config("${auth_name}SameSite")) { 164 if ($samesite =~ /\A(strict|lax)\z/i) { 165 $samesite = lc($1); 166 $string .= "; SameSite=$samesite"; 167 } 168 } 169 170 return $string; 171} 172 173 174sub decoded_requires { 175 my ($self, $r) = @_; 176 177 my $reqs = $r->requires or return; 178 my $encoding = $self->requires_encoding($r); 179 180 unless (is_blank($encoding)) { 181 for my $req (@$reqs) { 182 $$req{requirement} = Encode::decode($encoding, $$req{requirement}); 183 } 184 } 185 186 return $reqs; 187} 188 189 190sub decoded_user { 191 my ($self, $r) = @_; 192 193 my $user = $r->user; 194 195 if (is_blank($user)) { 196 return $user; 197 } 198 199 my $encoding = $self->encoding($r); 200 201 if (!is_blank($encoding)) { 202 $user = Encode::decode($encoding, $user); 203 } 204 205 return $user; 206} 207 208 209sub encoding { 210 my ($self, $r) = @_; 211 212 my $auth_name = $r->auth_name; 213 214 return $r->dir_config("${auth_name}Encoding"); 215} 216 217 218sub escape_uri { 219 my ($r, $string) = @_; 220 return Apache2::Util::escape_path($string, $r->pool); 221} 222 223 224sub get_cookie_path { 225 my ($self, $r) = @_; 226 227 my $auth_name = $r->auth_name; 228 229 return $r->dir_config("${auth_name}Path"); 230} 231 232 233sub handle_cache { 234 my ($self, $r) = @_; 235 236 my $auth_name = $r->auth_name; 237 238 return unless $auth_name; 239 240 unless ($r->dir_config("${auth_name}Cache")) { 241 $r->no_cache(1); 242 $r->err_headers_out->set(Pragma => 'no-cache'); 243 } 244} 245 246 247sub key { 248 my ($self, $r) = @_; 249 250 my $cookie_name = $self->cookie_name($r); 251 252 my $allcook = ($r->headers_in->get("Cookie") || ""); 253 254 return ($allcook =~ /(?:^|\s)$cookie_name=([^;]*)/)[0]; 255} 256 257 258sub login { 259 my ($self, $r) = @_; 260 261 my $debug = $r->dir_config("AuthCookieDebug") || 0; 262 263 my $auth_type = $r->auth_type; 264 my $auth_name = $r->auth_name; 265 266 my $params = $self->params($r); 267 268 if ($r->method eq 'POST') { 269 $self->_convert_to_get($r); 270 } 271 272 my $default_destination = $r->dir_config("${auth_name}DefaultDestination"); 273 my $destination = $params->param('destination'); 274 275 if (is_blank($destination)) { 276 if (!is_blank($default_destination)) { 277 $destination = $default_destination; 278 $r->server->log_error("destination set to $destination"); 279 } 280 else { 281 $r->server->log_error("No key 'destination' found in form data"); 282 $r->subprocess_env('AuthCookieReason', 'no_cookie'); 283 return $auth_type->login_form($r); 284 } 285 } 286 287 if ($r->dir_config("${auth_name}EnforceLocalDestination")) { 288 my $current_url = $r->construct_url; 289 unless (is_local_destination($destination, $current_url)) { 290 $r->server->log_error("non-local destination $destination detected for uri ",$r->uri); 291 292 if (is_local_destination($default_destination, $current_url)) { 293 $destination = $default_destination; 294 $r->server->log_error("destination changed to $destination"); 295 } 296 else { 297 $r->server->log_error("Returning login form: non local destination: $destination"); 298 $r->subprocess_env('AuthCookieReason', 'no_cookie'); 299 return $auth_type->login_form($r); 300 } 301 } 302 } 303 304 # Get the credentials from the data posted by the client 305 my @credentials; 306 for (my $i = 0; defined $params->param("credential_$i"); $i++) { 307 my $key = "credential_$i"; 308 my $val = $params->param($key); 309 $r->server->log_error("$key $val") if $debug >= 2; 310 push @credentials, $val; 311 } 312 313 # save creds in pnotes so login form script can use them if it wants to 314 $r->pnotes("${auth_name}Creds", \@credentials); 315 316 # Exchange the credentials for a session key. 317 my $ses_key = $self->authen_cred($r, @credentials); 318 unless ($ses_key) { 319 $r->server->log_error("Bad credentials") if $debug >= 2; 320 $r->subprocess_env('AuthCookieReason', 'bad_credentials'); 321 $r->uri($self->untaint_destination($destination)); 322 return $auth_type->login_form($r); 323 } 324 325 if ($debug >= 2) { 326 defined $ses_key ? $r->server->log_error("ses_key $ses_key") 327 : $r->server->log_error("ses_key undefined"); 328 } 329 330 $self->send_cookie($r, $ses_key); 331 332 $self->handle_cache($r); 333 334 if ($debug >= 2) { 335 $r->server->log_error("redirect to $destination"); 336 } 337 338 $r->headers_out->set( 339 "Location" => $self->untaint_destination($destination)); 340 341 return HTTP_MOVED_TEMPORARILY; 342} 343 344 345sub login_form { 346 my ($self, $r) = @_; 347 348 my $auth_name = $r->auth_name; 349 350 if ($r->method eq 'POST') { 351 $self->_convert_to_get($r); 352 } 353 354 # There should be a PerlSetVar directive that gives us the URI of 355 # the script to execute for the login form. 356 357 my $authen_script; 358 unless ($authen_script = $r->dir_config($auth_name . "LoginScript")) { 359 $r->server->log_error("PerlSetVar '${auth_name}LoginScript' not set", $r->uri); 360 return SERVER_ERROR; 361 } 362 363 my $status = $self->login_form_status($r); 364 $status = HTTP_FORBIDDEN unless defined $status; 365 366 $r->custom_response($status, $authen_script); 367 368 return $status; 369} 370 371 372sub login_form_status { 373 my ($self, $r) = @_; 374 375 my $ua = $r->headers_in->get('User-Agent') 376 or return HTTP_FORBIDDEN; 377 378 if (Apache::AuthCookie::Util::understands_forbidden_response($ua)) { 379 return HTTP_FORBIDDEN; 380 } 381 else { 382 return HTTP_OK; 383 } 384} 385 386 387sub logout { 388 my ($self,$r) = @_; 389 390 my $debug = $r->dir_config("AuthCookieDebug") || 0; 391 392 $self->remove_cookie($r); 393 394 $self->handle_cache($r); 395} 396 397 398sub params { 399 my ($self, $r) = @_; 400 401 return Apache2::AuthCookie::Params->new($r); 402} 403 404 405sub recognize_user { 406 my ($self, $r) = @_; 407 408 # only check if user is not already set 409 return DECLINED unless is_blank($r->user); 410 411 my $debug = $r->dir_config("AuthCookieDebug") || 0; 412 413 my $auth_type = $r->auth_type; 414 my $auth_name = $r->auth_name; 415 416 return DECLINED if is_blank($auth_type) or is_blank($auth_name); 417 418 return DECLINED if is_blank($r->headers_in->get('Cookie')); 419 420 my $cookie = $self->key($r); 421 my $cookie_name = $self->cookie_name($r); 422 423 $r->server->log_error("cookie $cookie_name is $cookie") 424 if $debug >= 2; 425 426 return DECLINED if is_blank($cookie); 427 428 my ($user,@args) = $auth_type->authen_ses_key($r, $cookie); 429 430 if (!is_blank($user) and scalar @args == 0) { 431 $r->server->log_error("user is $user") if $debug >= 2; 432 433 # send cookie with update expires timestamp if session timeout is on 434 if (my $expires = $r->dir_config("${auth_name}SessionTimeout")) { 435 $self->send_cookie($r, $cookie, {expires => $expires}); 436 } 437 438 $r->user( $self->_encode($r, $user) ); 439 } 440 elsif (scalar @args > 0 and $auth_type->can('custom_errors')) { 441 return $auth_type->custom_errors($r, $user, @args); 442 } 443 444 return is_blank($user) ? DECLINED : OK; 445} 446 447 448sub remove_cookie { 449 my ($self, $r) = @_; 450 451 my $cookie_name = $self->cookie_name($r); 452 453 my $debug = $r->dir_config("AuthCookieDebug") || 0; 454 455 my $str = $self->cookie_string( 456 request => $r, 457 key => $cookie_name, 458 value => '', 459 expires => 'Mon, 21-May-1971 00:00:00 GMT' 460 ); 461 462 $r->err_headers_out->add("Set-Cookie" => "$str"); 463 $r->server->log_error("removed cookie $cookie_name") if $debug >= 2; 464} 465 466 467sub requires_encoding { 468 my ($self, $r) = @_; 469 470 my $auth_name = $r->auth_name; 471 472 return $r->dir_config("${auth_name}RequiresEncoding"); 473} 474 475 476sub send_cookie { 477 my ($self, $r, $ses_key, $cookie_args) = @_; 478 479 $cookie_args = {} unless defined $cookie_args; 480 481 my $cookie_name = $self->cookie_name($r); 482 483 my $cookie = $self->cookie_string( 484 request => $r, 485 key => $cookie_name, 486 value => $ses_key, 487 %$cookie_args 488 ); 489 490 $self->send_p3p($r); 491 492 $r->err_headers_out->add("Set-Cookie" => $cookie); 493} 494 495 496sub send_p3p { 497 my ($self, $r) = @_; 498 499 my $auth_name = $r->auth_name; 500 501 if (my $p3p = $r->dir_config("${auth_name}P3P")) { 502 $r->err_headers_out->set(P3P => $p3p); 503 } 504} 505 506 507sub untaint_destination { 508 my ($self, $dest) = @_; 509 510 return Apache::AuthCookie::Util::escape_destination($dest); 511} 512 513# convert current request to GET 514sub _convert_to_get { 515 my ($self, $r) = @_; 516 517 return unless $r->method eq 'POST'; 518 519 my $debug = $r->dir_config("AuthCookieDebug") || 0; 520 521 $r->server->log_error("Converting POST -> GET") if $debug >= 2; 522 523 my $args = $self->params($r); 524 525 my @pairs = (); 526 527 for my $name ($args->param) { 528 # we dont want to copy login data, only extra data 529 next if $name eq 'destination' 530 or $name =~ /^credential_\d+$/; 531 532 for my $v ($args->param($name)) { 533 push @pairs, escape_uri($r, $name) . '=' . escape_uri($r, $v); 534 } 535 } 536 537 $r->args(join '&', @pairs) if scalar(@pairs) > 0; 538 539 $r->method('GET'); 540 $r->method_number(M_GET); 541 $r->headers_in->unset('Content-Length'); 542} 543 544sub _encode { 545 my ($self, $r, $value) = @_; 546 547 my $encoding = $self->encoding($r); 548 549 if (is_blank($encoding)) { 550 return $value; 551 } 552 else { 553 return Encode::encode($encoding, $value); 554 } 555} 556 5571; 558 559__END__ 560 561=pod 562 563=encoding UTF-8 564 565=head1 NAME 566 567Apache2::AuthCookie::Base - Common Methods Shared by Apache2 and Apache2_4 AuthCookie Subclasses. 568 569=head1 VERSION 570 571version 3.30 572 573=head1 DESCRIPTION 574 575This module contains common code shared by AuthCookie for Apache 2.x and Apache 2.4. 576 577=head1 METHODS 578 579=head2 authenticate($r): int 580 581This method is one you'll use in a server config file (httpd.conf, .htaccess, 582...) as a PerlAuthenHandler. If the user provided a session key in a cookie, 583the C<authen_ses_key()> method will get called to check whether the key is 584valid. If not, or if there is no key provided, we redirect to the login form. 585 586=head2 cookie_name($r): string 587 588Return the name of the auth cookie for this request. This is either 589C<${auth_name}CookieName>, or AuthCookie's self generated name. 590 591=head2 cookie_string(%args): string 592 593Generate a cookie string. C<%args> are: 594 595=over 4 596 597=item * 598 599request 600 601The Apache request object 602 603=item * 604 605key 606 607The Cookie name 608 609=item * 610 611value 612 613the Cookie value 614 615=item * 616 617expires (optional) 618 619When the cookie expires. See L<Apache::AuthCookie::Util/expires()>. Uses C<${auth_name}Expires> if not giv 620 621=back 622 623All other cookie settings come from C<PerlSetVar> settings. 624 625=head2 decoded_requires($r): arrayref 626 627This method returns the C<< $r->requires >> array, with the C<requirement> 628values decoded if C<${auth_name}RequiresEncoding> is in effect for this 629request. 630 631=head2 decoded_user($r): string 632 633If you have set ${auth_name}Encoding, then this will return the decoded value of 634C<< $r-E<gt>user >>. 635 636=head2 encoding($r): string 637 638Return the ${auth_name}Encoding setting that is in effect for this request. 639 640=head2 escape_uri($r, $value): string 641 642Escape the given string so it is suitable to be used in a URL. 643 644=head2 get_cookie_path($r): string 645 646Returns the value of C<PerlSetVar ${auth_name}Path>. 647 648=head2 handle_cache($r): void 649 650If C<${auth_name}Cache> is defined, this sets up the response so that the 651client will not cache the result. This sents C<no_cache> in the apache request 652object and sends the appropriate headers so that the client will not cache the 653response. 654 655=head2 key($r): string 656 657This method will return the current session key, if any. This can be handy 658inside a method that implements a C<require> directive check (like the 659C<species> method discussed above) if you put any extra information like 660clearances or whatever into the session key. 661 662=head2 login($r): int 663 664This method handles the submission of the login form. It will call the 665C<authen_cred()> method, passing it C<$r> and all the submitted data with names 666like C<credential_#>, where # is a number. These will be passed in a simple 667array, so the prototype is C<$self-E<gt>authen_cred($r, @credentials)>. After 668calling C<authen_cred()>, we set the user's cookie and redirect to the URL 669contained in the C<destination> submitted form field. 670 671=head2 login_form($r): int 672 673This method is responsible for displaying the login form. The default 674implementation will make an internal redirect and display the URL you specified 675with the C<PerlSetVar WhatEverLoginScript> configuration directive. You can 676overwrite this method to provide your own mechanism. 677 678=head2 login_form_status($r): int 679 680This method returns the HTTP status code that will be returned with the login 681form response. The default behaviour is to return HTTP_FORBIDDEN, except for 682some known browsers which ignore HTML content for HTTP_FORBIDDEN responses 683(e.g.: SymbianOS). You can override this method to return custom codes. 684 685Note that HTTP_FORBIDDEN is the most correct code to return as the given 686request was not authorized to view the requested page. You should only change 687this if HTTP_FORBIDDEN does not work. 688 689=head2 logout($r): void 690 691This is simply a convenience method that unsets the session key for you. You 692can call it in your logout scripts. Usually this looks like 693C<$r-E<gt>auth_type-E<gt>logout($r)>. 694 695=head2 params($r): Apache2::AuthCookie::Params 696 697Get the GET/POST params object for this request. 698 699=head2 recognize_user($r): int 700 701If the user has provided a valid session key but the document isn't protected, 702this method will set C<$r-E<gt>user> anyway. Use it as a PerlFixupHandler, 703unless you have a better idea. 704 705=head2 remove_cookie($r): void 706 707Adds a C<Set-Cookie> header that instructs the client to delete the cookie 708immediately. 709 710=head2 requires_encoding($r): string 711 712Return the ${auth_name}RequiresEncoding setting that is in effect for this request. 713 714=head2 send_cookie($r, $ses_key, $args): void 715 716By default this method simply sends out the session key you give it. If you 717need to change the default behavior (perhaps to update a timestamp in the key) 718you can override this method. 719 720=head2 send_p3p($r): void 721 722Set a P3P response header if C<${auth_name}P3P> is configured. The value of 723the header is whatever is in the C<${auth_name}P3P> setting. 724 725=head2 untaint_destination($destination): string 726 727This method returns a modified version of the destination parameter before 728embedding it into the response header. Per default it escapes CR, LF and TAB 729characters of the uri to avoid certain types of security attacks. You can 730override it to more limit the allowed destinations, e.g., only allow relative 731uris, only special hosts or only limited set of characters. 732 733=for Pod::Coverage OK 734 DECLINED 735 SERVER_ERROR 736 M_GET 737 HTTP_FORBIDDEN 738 HTTP_MOVED_TEMPORARILY 739 HTTP_OK 740 741=head1 SOURCE 742 743The development version is on github at L<https://https://github.com/mschout/apache-authcookie> 744and may be cloned from L<git://https://github.com/mschout/apache-authcookie.git> 745 746=head1 BUGS 747 748Please report any bugs or feature requests on the bugtracker website 749L<https://github.com/mschout/apache-authcookie/issues> 750 751When submitting a bug or request, please include a test-file or a 752patch to an existing test-file that illustrates the bug or desired 753feature. 754 755=head1 AUTHOR 756 757Michael Schout <mschout@cpan.org> 758 759=head1 COPYRIGHT AND LICENSE 760 761This software is copyright (c) 2000 by Ken Williams. 762 763This is free software; you can redistribute it and/or modify it under 764the same terms as the Perl 5 programming language system itself. 765 766=cut 767