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