File Coverage

blib/lib/Apache2/Controller/Auth/OpenID.pm
Criterion Covered Total %
statement 18 18 100.0
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 24 24 100.0


line stmt bran cond sub pod time code
1             package Apache2::Controller::Auth::OpenID;
2              
3             =head1 NAME
4              
5             Apache2::Controller::Auth::OpenID - OpenID for Apache2::Controller
6              
7             =head1 VERSION
8              
9             Version 1.001.001 - THIS MODULE DISABLED, DOES NOT WORK.
10              
11             =cut
12              
13 1     1   1472 use version;
  1         2  
  1         7  
14             our $VERSION = version->new('1.001.001');
15              
16             =head1 SYNOPSIS
17              
18             PerlLoadModule Apache2::Controller::Directives
19              
20            
21             SetHandler modperl
22              
23             # uri to your login controller:
24             A2C_Auth_OpenID_Login login
25              
26             # uri to your logout controller:
27             A2C_Auth_OpenID_Logout logout
28              
29             # uri to your registration controller:
30             A2C_Auth_OpenID_Register register
31             # you might want to put this outside the protected area,
32             # i.e. /other/register - you can use leading '/' for absolute uri
33              
34             # idle timeout in seconds, +2m, +3h, +4D, +6M, +7Y, or 'no timeout'
35             # default is 1 hour. a month is actually 30 days, a year 365.
36             A2C_Auth_OpenID_Timeout +1h
37              
38             # name of the openid table in database:
39             A2C_Auth_OpenID_Table openid
40            
41             # key of the username field in table:
42             A2C_Auth_OpenID_User_Field uname
43              
44             # key of the openid url field in table:
45             A2C_Auth_OpenID_URL_Field openid_url
46              
47             # if you use multiple DBI handles, name the one in pnotes
48             # that you should use for reading the openid table:
49             A2C_Auth_OpenID_DBI_Name dbh
50              
51             # by default trust_root is the result of $r->construct_url(''),
52             # i.e. the top of the site (see Apache::URI)
53             A2C_Auth_OpenID_Trust_Root http://myapp.tld/somewhere
54              
55             # set a random string used as salt with time() to sha secret
56             A2C_Auth_OpenID_Consumer_Secret
57              
58             # but that random salt will be reset if you restart server,
59             # which may cause current logins to die, so you can specify
60             # your own constant salt of arbitrary length
61             A2C_Auth_OpenID_Consumer_Secret abcdefg1234567
62              
63             # if you do not want to preserve GET/POST params
64             # across redirects to the OpenID server, use this flag:
65             # A2C_Auth_OpenID_NoPreserveParams
66              
67             # if you do not overload get_uname() (see below), then
68             # PerlHeaderParserHandlers must be invoked in order
69             # to set up the dbi handle before checking auth
70             # with the default method. In this example,
71             # MyApp::DBI::Connector is an Apache2::Controller::DBI::Connector
72             # and MyApp::Session is an Apache2::Controller::Session::Cookie...
73             # see those modules for more info.
74              
75             PerlInitHandler MyApp::Dispatch
76             PerlHeaderParserHandler MyApp::DBI::Connector
77             PerlHeaderParserHandler MyApp::Session
78             PerlHeaderParserHandler Apache2::Controller::Auth::OpenID
79            
80              
81             =head1 DESCRIPTION
82              
83             Implements an authentication mechanism for L
84             that uses OpenID.
85              
86             This is NOT an AuthenPerlHandler. This is an implementation
87             of a simple cookie-based mechanism that shows the browser
88             a login page, where your controller should present and process an
89             HTML form for logging in.
90              
91             If you want an authentication handler that uses browser-based auth
92             (the pop-up dialog implemented by HTTP auth protocol) use
93             L, which is not a part of Apache2::Controller
94             but should work for you anyway.
95              
96             Natively this depends on L
97             and L being configured
98             correctly, but you could always subclass this and overload the
99             methods below to get information from other sources.
100              
101             If no claimed ID is detected, the user is shown the login
102             page. If an error occured, you'll find the L
103             error details in the session under C<< {a2c}{openid}{errtext} >>
104             and C<< {a2c}{openid}{errcode} >>.
105              
106             =head2 REDIRECTION OR REDISPATCH?
107              
108             Whether redirecting or redispatching, stuff has to be saved
109             in the session, so C<< $r->notes->{a2c}{session_force_save} >>
110             will be set.
111              
112             =head3 INTERNAL LOGIN, LOGOUT AND REGISTER PAGES
113              
114             =head4 RELATIVE URIS - REDISPATCH
115              
116             If the uris for these pages are relative, not absolute, i.e.
117             they are handled by the same controller that we're going to
118             anyway, then it
119             tries setting the uri and re-dispatching by grabbing the dispatch
120             class name out of C<< $r->pnotes->{a2c}{dispatch_class} >> and
121             instantiating a new dispatch handler object.
122              
123             (Dispatch can't keep the handler
124             subref around in pnotes due to circular references, or reliably assume
125             that we know at what location in
126             the C<< PerlInitHandler >> stack the dispatch handler coderef was
127             stored by Apache, so we just create a new one - this is assured
128             to be faster than creating an entire new request, which would
129             do that anyway.)
130              
131             So in this case, the content for the login, logout, or register pages will
132             appear even though the browser uri still displays the requested
133             protected URI.
134              
135             =head4 ABSOLUTE URIS - REDIRECT
136              
137             If the uris for the internal pages are absolute, i.e.
138             they might be handled by a different controller than the
139             one that was dispatched, a redirect using Location HTTP header
140             is used.
141              
142             =head3 EXTERNAL OPENID PAGES
143              
144             Any time the browser needs to go to an external page (the openid server),
145             a redirect using a Location: HTTP header is used.
146              
147             =head2 PRESERVATION OF INITIAL REQUEST
148              
149             =head3 REQUESTED URI
150              
151             When it goes to your login, or register page, it
152             stashes the user's uri into the session as
153             C<< {a2c}{openid}{previous_uri} >>
154             and should preserve this for the return url. It uses
155             C<< $r->construct_url() >> as the trusted root.
156              
157             When the user passes the authentication process (C<< $csr->verified_identity >>),
158             it sets C<< $r->pnotes->{a2c}{openid_logged_in} >> for this request
159             to let your handler know if you want to display a message like
160             "You have successfully logged in" or something.
161              
162             =head3 GET AND POST VARS
163              
164             The whole point of OpenID is that the login mechanism is
165             invisible - as long as the user can claim to own the url,
166             and the auth server returns a positive response, then the
167             user's session should continue.
168              
169             So, this preserves GET parmas and the POST body through the
170             login process and the redirect sequence to the OpenID server,
171             including when the local session times out. If they come back
172             after a while and click a submit button, but either their
173             local session has timed out or their OpenID server session
174             has timed out through whatever mechanism that uses, then after
175             they log into OpenID and are redirected back to the protected
176             area, the GET params and POST body are restored, and it will
177             do what the user expected when they clicked the submit button.
178              
179             This behavior
180             is a feature, so it is enabled by default, but it may not
181             be expected, so you can turn it off by using the directive
182             flag C<< A2C_Auth_OpenID_NoPreserveParams >>.
183              
184             =head1 DIRECTIVES
185              
186             L
187              
188             =head1 CACHING
189              
190             If you want to provide a cache for L
191             to pass onto L, subclass this module and implement
192             a method C<< cache() >> that returns the appropriate cache object.
193              
194             =head1 CAVEATS
195              
196             I have heard there are trickier things one can do to ensure the
197             security of a session based cookie. This module just implements
198             a simple association of a user with a session key by storing a flag
199             and a last-accessed time value in the session hash, nothing fancier.
200             If you have recommendations, please let me know.
201              
202             This calls C<< $r->connection->get_remote_host >> and saves it
203             in the C<< {a2c}{openid} >> section of the session hash. So if
204             you don't want it to do DNS lookups, set directive C<< HostNameLookups off >>.
205              
206             =cut
207              
208 1     1   133 use strict;
  1         2  
  1         35  
209 1     1   6 use warnings FATAL => 'all';
  1         2  
  1         39  
210 1     1   5 use English '-no_match_vars';
  1         3  
  1         12  
211              
212 1     1   601 use Carp qw( longmess );
  1         2  
  1         58  
213              
214 1         189 use base qw(
215             Apache2::Controller::NonResponseRequest
216 1     1   5 );
  1         2  
217              
218             use Log::Log4perl qw(:easy);
219             use YAML::Syck;
220             use Digest::SHA qw( sha224_base64 );
221             use Net::OpenID::Consumer;
222             use URI;
223             use List::MoreUtils qw(any);
224              
225             use Apache2::Const -compile => qw( OK SERVER_ERROR REDIRECT );
226              
227             use Apache2::Controller::X;
228              
229             =head2 new
230              
231             Overloaded constructor will always throw an L
232             because this module does not work.
233              
234             =cut
235              
236             sub new {
237             a2cx __PACKAGE__." is disabled, does not work, do not use.";
238             }
239              
240             # hopefully we get the same default consumer secret as in top level
241             use Apache2::Controller::Const qw( $DEFAULT_CONSUMER_SECRET );
242              
243             =head1 OVERLOADABLE METHODS
244              
245             The only method which should be overloaded in your subclass
246             is C<< get_uname( $openid_url ) >> which returns the username
247             string that corresponds to the openid url supplied by the cookie.
248             When overloading, you get the RequestRec in C<< $self->{r} >>.
249              
250             =head2 get_uname
251              
252             my $uname = $self->get_uname($openid_url);
253              
254             Takes a string which is the supplied openid_url.
255             You can overload C<< get_uname >> to supply it
256             by some other means, such as by LDAP.
257              
258             =cut
259              
260             sub get_uname {
261             my ($self, $openid_url) = @_;
262              
263             a2cx "get_uname() requires an openid_url string param"
264             if !$openid_url || ref $openid_url;
265              
266             my $conf = $self->{conf};
267              
268             my $pnotes = $self->pnotes;
269             DEBUG sub { "pnotes: ".Dump($pnotes) };
270              
271             my $dbh = $self->pnotes->{a2c}{ $conf->{dbi_name} }
272             || a2cx "Database handle '$conf->{dbi_name}' is not connected in pnotes"
273             ." for default handler ".__PACKAGE__;
274              
275             my $uname;
276             eval {
277             ($uname) = $dbh->selectrow_array(
278             qq| SELECT $conf->{user_field}
279             FROM $conf->{table}
280             WHERE $conf->{url_field} = ?
281             |, undef, $openid_url
282             );
283             };
284             a2cx "Error in default get_uname() from dbh: $EVAL_ERROR"
285             if $EVAL_ERROR;
286              
287             return $uname;
288             }
289              
290             =head1 INTERNAL METHODS
291              
292             "These aren't the methods you're looking for."
293              
294             "These aren't the methods we're looking for."
295              
296             "He can go about his business."
297              
298             "You can go about your business."
299              
300             "Move along."
301              
302             "Move along. Move along."
303              
304             You don't access these methods. This is internal documentation.
305              
306             =head2 default_directives
307              
308             Calculate and return the hash of directive defaults.
309             (Some of these are based on the current of the handler.)
310              
311             =cut
312              
313             sub default_directives {
314             my ($self) = @_;
315             return (
316             login => "login",
317             logout => "logout",
318             register => "register",
319             timeout => 3600,
320             table => 'openid',
321             user_field => 'uname',
322             url_field => 'openid_url',
323             trust_root => $self->construct_url(q{}),
324             dbi_name => 'dbh',
325             lwp_class => 'LWPx::ParanoidAgent',
326             lwp_opts => {
327             timeout => 20,
328             },
329             consumer_secret => $DEFAULT_CONSUMER_SECRET,
330             nopreserveparams => 0,
331             );
332             }
333              
334             =head2 openid_url_normalize
335              
336             Correct trailing double /'s etc. in the openid url.
337              
338             =cut
339              
340             sub openid_url_normalize {
341             my ($self, $openid_url) = @_;
342             my $orig_url = $openid_url;
343             my ($scheme) = $openid_url =~ m{ \A (\w+) : // }mxs;
344             $scheme ||= 'http';
345             $scheme = lc $scheme;
346             $openid_url = URI->new( $openid_url, $scheme )->canonical->as_string
347             || a2cx "Could not normalize openid_url '$orig_url'";
348             $openid_url =~ s{ /+ \z }{}mxs;
349             return $openid_url;
350             }
351              
352             =head2 process
353              
354             Make sure the config directives are assigned or use defaults.
355              
356             If uri = login uri, process accordingly.
357              
358             If uri = logout uri, delete session hash login flags and return OK.
359              
360             =cut
361              
362             sub _save_errs_in_sess {
363             my ($self, $openid_csr) = @_;
364             my $sess = $self->pnotes->{a2c}{session};
365             my $errcode = $openid_csr->errcode
366             || $openid_csr->{last_errcode} || '[ no error code ]';
367             my $errtext = $openid_csr->err
368             || $openid_csr->{last_errtext} || '[ no error text ]';
369             $sess->{a2c}{openid}{errtext} = $errtext;
370             $sess->{a2c}{openid}{errcode} = $errcode;
371             return ($errtext, $errcode);
372             }
373              
374             my ($openid_csr, $consumer_secret_string);
375             my %params_hash;
376              
377             sub process {
378             my ($self) = @_;
379             my $uri = $self->uri();
380              
381             # my $pnotes = $self->pnotes;
382             # DEBUG sub { "Before checking session, pnotes is:\n".Dump($pnotes) };
383              
384             # make sure a session object is set up already
385             my $sess = $self->pnotes->{a2c}{session}
386             || a2cx "No session object configured for handler";
387              
388             DEBUG sub { "Entering, processing uri '$uri'.\nsession is:\n".Dump($sess) };
389              
390             my $directives = $self->get_directives();
391             my %conf = (
392             $self->default_directives(),
393             ( map {(lc($_) => $directives->{"A2C_Auth_OpenID_$_"})}
394             grep exists $directives->{"A2C_Auth_OpenID_$_"}, qw(
395             Login Logout Register Timeout
396             Table User_Field URL_Field DBI_Name
397             Trust_Root LWP_Class Allow_Login Consumer_Secret
398             NoPreserveParams
399             ) ),
400             );
401              
402             # make a lookup verification map of the internal uris
403             $conf{is_internal} = { map {($conf{$_} => 1)} qw( login logout register ) };
404              
405             # slap in anything specified in the sub-hash of LWP class options
406             my $lwp_opts_directive = $directives->{A2C_Auth_OpenID_LWP_Opts} || { };
407             my @lwp_opt_keys = keys %{$lwp_opts_directive};
408             DEBUG sub { "Trying to slice in lwp_opts: ".Dump($lwp_opts_directive) };
409             @{$conf{lwp_opts}}{@lwp_opt_keys} = @{$lwp_opts_directive}{@lwp_opt_keys}
410             if scalar @lwp_opt_keys;
411              
412             $self->{conf} = \%conf;
413              
414             DEBUG sub { "conf:\n".Dump(\%conf) };
415             DEBUG sub { "session:\n".Dump($self->pnotes->{a2c}{session}) };
416              
417             # if we're on the register page, allow it through
418             return Apache2::Const::OK if $uri eq $self->qualify_uri($conf{register});
419              
420             # logout and return if we're processing the logout uri
421             if ($uri eq $self->qualify_uri($conf{logout})) {
422             DEBUG "requested logout page $conf{logout}, returning logout()";
423             return $self->logout();
424             }
425              
426             # return OK if their session is logged in and timestamp is current
427             if ($self->is_logged_in) {
428             DEBUG "user is logged in, returning OK";
429             return Apache2::Const::OK;
430             }
431             else {
432             DEBUG "user is NOT logged in, continuing auth";
433             }
434              
435             # consumer object creation is very slow, so we cache in package space:
436             if (!defined $openid_csr) {
437             my $cache = $self->can('cache') ? $self->cache() : undef;
438             eval "use $conf{lwp_class}";
439             a2cx "Could not load A2C_Auth_OpenID_LWP_Class ($conf{lwp_class}): "
440             ."$EVAL_ERROR" if $EVAL_ERROR;
441              
442             # stash string in package to avoid closure circle on this req's %conf.
443             # we provide some hardcoded junk if they didn't use the directive
444             # to specify or generate some.
445             $consumer_secret_string = $conf{consumer_secret};
446             DEBUG "Setting up CSR with secret string '$consumer_secret_string'";
447              
448             $openid_csr = Net::OpenID::Consumer->new(
449             ua => $conf{lwp_class}->new(%{ $conf{lwp_opts} }),
450             cache => $cache,
451             consumer_secret => sub {
452             my ($time) = @_;
453             return sha224_base64("$time-$consumer_secret_string");
454             },
455             debug => \&DEBUG,
456             args => sub {
457             my ($param_name) = @_;
458             return wantarray
459             ? @{ $params_hash{$param_name} }
460             : $params_hash{$param_name}[0];
461             },
462             );
463             }
464              
465             # we have to populate a package space variable params_hash
466             # with the params contents, so we won't create a closure
467             # that includes the request object when we construct the
468             # params subroutine for the openid csr object
469             my @param_names = $self->param;
470             %params_hash = map {
471             my @vals = $self->param($_);
472             ($_ => \@vals);
473             } @param_names;
474             DEBUG sub {
475             "params:\n".Dump(\%params_hash);
476             };
477            
478             my $openid_url = $self->param('openid_url') || $sess->{a2c}{openid}{openid_url};
479              
480             if ($openid_url) {
481             # if there is a param 'openid.identity' from a redirect
482             # from the openid server, make sure it is the same as
483             # the one we thing we're logging in for, else redirect to login
484             if (my $id_from_server = $self->param('openid.identity')) {
485             if ($openid_url ne $id_from_server) {
486             DEBUG "openid_url '$openid_url' does not match "
487             . "id from server '$id_from_server', redirect to login";
488             return $self->redirect_to($conf{login});
489             }
490             }
491              
492             # save the openid url in the session
493             $sess->{a2c}{openid}{openid_url} = $openid_url;
494             }
495             else {
496             DEBUG "no openid url detected, redirecting to login page";
497             return $self->redirect_to($conf{login});
498             }
499              
500             $openid_url = $self->{openid_url} = $self->openid_url_normalize($openid_url);
501              
502             # first verify that we know about this openid url, and redirect to
503             # the registration page if we don't
504              
505             DEBUG "looking for uname from openid table using openid_url '$openid_url'";
506              
507             my $uname = $self->get_uname($openid_url);
508              
509             if (!$uname) {
510             DEBUG("no uname! ... ".(defined $uname ? "'$uname'" : '[undef]'));
511             return $self->redirect_to($conf{register});
512             }
513              
514             $self->{uname} = $uname;
515              
516             DEBUG "Trying authentication for known user: $uname, $openid_url";
517             # okay, handle the authentication
518              
519             my $claimed_id;
520              
521             my $allow_login = $conf{allow_login};
522              
523             if (!$allow_login) {
524             $claimed_id = ($openid_csr->claimed_identity($openid_url) || '');
525             DEBUG sub {"claimed_id: ".(defined $claimed_id ? $claimed_id : '[undef]')};
526              
527             # if claimed id found, make sure session csr errors are cleared
528             if ($claimed_id) {
529             delete @{ $sess->{a2c}{openid} }{qw( errtext errcode )};
530             }
531             # otherwise put the errors in the session and redirect to login
532             else {
533             my ($errtext, $errcode) = $self->_save_errs_in_sess($openid_csr);
534             DEBUG "Claimed ID '$self->{openid_url}' is not an OpenID: "
535             . "($errcode) '$errtext'";
536             $self->redirect_to($conf{login});
537             }
538             }
539              
540             my $vident;
541             DEBUG "proceeding with authentication for uri '$uri'...";
542              
543             # we have to do this again?
544             $openid_csr->args(sub { return $self->param(@_) });
545              
546             if ($allow_login || ($vident = $openid_csr->verified_identity)) {
547             my $verified_url = $allow_login ? $openid_url : $vident->url;
548             DEBUG sub { "verifd ident: ".(defined $vident ? "'$vident'" : '[undef]') };
549             $openid_url = $self->openid_url_normalize($verified_url);
550              
551             my $connection = $self->connection;
552              
553             my $openid_sess = $sess->{a2c}{openid} ||= { };
554              
555             # update the session
556             $openid_sess->{logged_in} = 1;
557             $openid_sess->{last_accessed_time} = time;
558             $openid_sess->{remote_host} = $connection->get_remote_host();
559             $openid_sess->{remote_ip} = $connection->remote_ip();
560             $openid_sess->{openid_url} = $openid_url;
561              
562             # restore the saved query params and post body
563             $self->args($openid_sess->{previous}{get_args})
564             if $openid_sess->{previous}{get_args};
565              
566             if (my $post_params = $openid_sess->{previous}{post_params}) {
567             }
568              
569             # if everything works and we're returning the user okay,
570             # make sure to delete the previous url from session hash
571             delete $openid_sess->{previous};
572              
573             # set a flag for this request so controller can print
574             # a "login successful" message
575             $self->pnotes->{a2c}{openid_logged_in} = 1;
576              
577             $self->user($uname);
578              
579             return Apache2::Const::OK;
580             }
581             elsif (!$self->param('oic.time')) {
582              
583             # figure out what uri they're returning to.
584             # if we can't figure one out from session,
585             # they go back to the login page.
586             my $return_uri = $sess->{a2c}{openid}{previous}{uri};
587             DEBUG "previous_uri from sess is ".($return_uri || '[none]');
588              
589             my %qual_uris = map {($_ => $self->qualify_uri($conf{$_}))}
590             qw( login logout register );
591              
592             # depending on what the return uri was supposed to be, set
593             # it, or maybe the login uri, or maybe the current uri
594             my $real_return_uri
595             = $return_uri ? $return_uri
596             : $uri eq $qual_uris{logout} ? $qual_uris{login} # wrong
597             : $uri;
598              
599             # make sure we save the uri in any case?
600             $sess->{a2c}{openid}{previous}{uri} = $real_return_uri;
601              
602             my $return_to = $self->construct_url($real_return_uri);
603             DEBUG "calling claimed_identity->check_url with return_to '$return_to'";
604             DEBUG sub { "openid part of session is ".Dump($sess->{a2c}{openid}) };
605              
606             my $check_url = $claimed_id->check_url(
607             trust_root => $conf{trust_root},
608             return_to => $return_to,
609             ) || a2cx "Detected no check url from claimed ID";
610              
611             DEBUG "got back check_url '$check_url'";
612             return $self->redirect_to($check_url);
613             }
614             elsif (my $setup_url = $self->param('openid.user_setup_url')) {
615             DEBUG "redirecting to openid provider setup_url '$setup_url'";
616             return $self->redirect_to($setup_url);
617             }
618             elsif ($openid_csr->user_cancel) {
619             # redirect to login page
620             DEBUG "user cancelled: redirecting to login";
621             return $self->redirect_to($conf{login});
622             }
623             else {
624             my ($errtext, $errcode) = $self->_save_errs_in_sess($openid_csr);
625             DEBUG "Error for '$self->{openid_url}': ($errcode) '$errtext'";
626             a2cx "Error in OpenID authentication: ($errcode) '$errtext'";
627             }
628             }
629              
630             =head2 DESTROY
631              
632             To save memory and be clean, when the object is destroyed,
633             the package-space var C<< %params_hash >> is cleared.
634              
635             C<< process() >> has to populate a package space variable
636             C<< %params_hash >>
637             with the params contents, so we won't create a closure
638             that includes the request object when we construct the
639             params subroutine for the cached openid CSR object. Otherwise,
640             the CSR keeps a reference to our handler object around,
641             which contains a reference to the request object, and
642             then neither the request nor the handler object are
643             cleaned up after this handler exits. (Apparently
644             Apache doesn't execute DESTROY until the next time it
645             has to run this handler. Doesn't quite make sense,
646             but that's the way it behaved.)
647              
648             =cut
649              
650             sub DESTROY {
651             my ($self) = @_;
652             %params_hash = ();
653             }
654              
655             =head2 qualify_uri
656              
657             If the uri is relative, qualifies it by prepending current location.
658             Otherwise just returns the uri.
659              
660             =cut
661              
662             sub qualify_uri {
663             my ($self, $uri) = @_;
664             a2cx "only use this with in-server path uri's" if $uri =~ m{ \A \w+ : // }mxs;
665             $uri = $self->location.'/'.$uri if substr($uri, 0, 1) ne '/';
666             return $uri;
667             }
668              
669             =head2 redirect_to
670              
671             return $self->redirect_to($uri);
672              
673             If one of the three internal URIs, use C<< redispatch() >>.
674              
675             Otherwise, use C<< location_redirect >>.
676              
677             =cut
678              
679             sub redirect_to {
680             my ($self, $where_uri) = @_;
681             a2cx "Undefined redirect" if !defined $where_uri;
682              
683             my $conf = $self->{conf};
684             my $current_uri = $self->uri();
685              
686             return exists $conf->{is_internal}{$where_uri}
687             ? $self->redispatch($where_uri)
688             : $self->location_redirect($where_uri);
689             }
690              
691             =head2 location_redirect
692              
693             return $self->location_redirect($uri);
694              
695             Set the Location header and return REDIRECT.
696              
697             Forces the session to be saved in the cleanup handler.
698              
699             =cut
700              
701             sub location_redirect {
702             my ($self, $uri) = @_;
703              
704             DEBUG "redirecting with location header to $uri";
705              
706             $self->err_headers_out->add( Location => $uri );
707              
708             # set the flag to force the session to be saved
709             $self->pnotes->{a2c}{session_force_save} = 1;
710              
711             return Apache2::Const::REDIRECT;
712             }
713              
714             =head2 redispatch
715              
716             return $self->redispatch($uri);
717              
718             For the internal pages (login, logout, register), if they are
719             relative, re-dispatch them and return OK, else if absolute,
720             set location and return redirect.
721              
722             If where == register or login, and the current uri is not
723             register or login, stash the current uri in
724             C<< session->{a2c}{openid}{previous}{uri} >>, and if
725             C<< A2C_Auth_OpenID_NoPreserveParams >> is NOT set,
726             then it stashes the get args and post body in C<< ...{previous}{get} >>
727             and C<< ...{previous}{post} >> for reattaching to the request
728             after successful authentication on the return from the auth server.
729              
730             =cut
731              
732             sub redispatch {
733             my ($self, $where_uri) = @_;
734              
735             DEBUG "redispatch uri '$where_uri'?";
736              
737             # if it's an absolute path or schemed url, use a location redirect
738             return $self->location_redirect($where_uri)
739             if $where_uri =~ m{ \A / }mxs
740             || $where_uri =~ m{ \A \w+ :// }mxs;
741              
742             my $conf = $self->{conf};
743             my $current_uri = $self->uri();
744             my $current_loc = $self->location();
745             (my $current_relative_uri = $current_uri) =~ s{ \A \Q$current_loc\E / }{}mxs;
746             DEBUG "current_relative_uri '$current_relative_uri'";
747              
748             # save the current uri, get vars and post body in session
749             my $register_uri = $conf->{register};
750             my $login_uri = $conf->{login};
751             if ( ($where_uri eq $register_uri || $where_uri eq $login_uri)
752             && $current_relative_uri ne $register_uri
753             && $current_relative_uri ne $login_uri
754             ) {
755             DEBUG "setting session previous_uri to '$current_uri'";
756             $self->pnotes->{a2c}{session}{a2c}{openid}{previous}{uri} = $current_uri;
757             $self->preserve_params unless $conf->{nopreserveparams};
758             }
759              
760             # now set the new URI and redispatch.
761              
762             DEBUG "redispatching...";
763              
764             my $loc = $self->location;
765             # DEBUG "loc is first '$loc'";
766             $loc =~ s{ /+ \z }{}mxs;
767             # DEBUG "loc is now '$loc'";
768              
769             my $where_full_uri = "$loc/$where_uri";
770             DEBUG "Trying to redispatch to full uri '$where_full_uri'";
771              
772             $self->uri($where_full_uri);
773              
774             my $dispatch_class = $self->pnotes->{a2c}{dispatch_class}
775             || a2cx 'No dispatch class saved in $r->pnotes->{a2c}{dispatch_class}';
776            
777             # # clear the previously set response handler
778              
779             # redispatch
780              
781             # we trap errors, but we use a location redirect if we encounter any
782             # we skip the 'process' subroutine because it uses set_handler,
783             # which seems like it should work, but in fact stalls the request
784             # after the last cleanup handler completes, even though it seems
785             # like everything completed successfully.
786             my $previously_set_controller = $self->pnotes->{a2c}{controller}
787             || a2cx "no controller previously dispatched in pnotes->{a2c}{controller}";
788              
789             eval {
790             my $redispatch_handler = $dispatch_class->new($self->{r});
791             $redispatch_handler->find_controller;
792             my $redispatch_controller = $self->pnotes->{a2c}{controller}
793             || a2cx "Redispatch set no new controller in pnotes->{a2c}{controller}";
794             a2cx "Redispatch controller '$redispatch_controller' is not previously "
795             ."set controller '$previously_set_controller'"
796             if $redispatch_controller ne $previously_set_controller;
797             };
798             if (my $X = Exception::Class->caught('Apache2::Controller::X')) {
799             WARN "Caught Apache2::Controller::X trying to redispatch $where_full_uri";
800             WARN(ref($X).": $X\n".($X->dump ? Dump($X->dump) : '').$X->trace());
801             return $self->location_redirect($where_uri);
802             }
803             elsif ($EVAL_ERROR) {
804             WARN "Unknown error trying to redispatch $where_full_uri: $EVAL_ERROR";
805             return $self->location_redirect($where_uri);
806             }
807              
808             return Apache2::Const::OK;
809             }
810              
811             =head2 preserve_params
812              
813             Preserve the GET and POST params in the session.
814              
815             =cut
816              
817             sub preserve_params {
818             my ($self) = @_;
819             my $conf = $self->{conf};
820              
821             my $previous = $self->pnotes->{a2c}{session}{a2c}{openid}{previous} ||= { };
822             $previous->{get_args} = $self->args;
823              
824             # if we're not POSTing, just return - ??
825             return if !$self->method eq 'POST';
826              
827             # get the POST body table,
828             # and get the params directly from it, so we don't mix in
829             # any GET params and keep them straight.
830             my $post_body = $self->body;
831             my @post_keys = keys %{$post_body};
832             my %post_params = map {
833             my @vals = $self->body($_);
834             ($_ => @vals > 1 ? \@vals : $vals[0]);
835             } keys %{$post_body};
836            
837             $previous->{post_params} = \%post_params;
838             return;
839             }
840              
841             =head2 is_logged_in
842              
843             Check the fields in the session hash to make sure they're logged in.
844             Apply the directive timeout to make sure. Don't change anything though.
845             Just return if not logged in, or return 1 if logged in.
846              
847             =cut
848              
849             sub is_logged_in {
850             my ($self) = @_;
851             my $sess = $self->pnotes->{a2c}{session};
852              
853             my $openid_sess = $self->pnotes->{a2c}{session}{a2c}{openid};
854              
855             DEBUG sub { "openid part of session is ".Dump($openid_sess) };
856              
857             return if !$openid_sess->{logged_in};
858            
859             my $last_accessed = $openid_sess->{last_accessed_time};
860             return if !defined $last_accessed;
861              
862             my $conf = $self->{conf};
863             my $timeout = $conf->{timeout};
864              
865             if ($timeout eq 'no timeout') {
866             $self->user($self->{uname});
867             return 1;
868             }
869              
870             my $current_time = time;
871              
872             DEBUG "comparing last accessed '$last_accessed' to current time '$current_time' with timeout '$timeout'";
873              
874             if ($current_time - $timeout > $last_accessed) {
875             DEBUG "login session has timed out";
876             $self->pnotes->{a2c}{session}{a2c}{openid}{previous_uri} = $self->uri;
877             return;
878             }
879              
880             $self->user($self->{uname});
881             return 1;
882             }
883              
884             =head2 logout
885              
886             Log the user out by clearing the relevant fields in the session hash.
887              
888             =cut
889              
890             sub logout {
891             my ($self) = @_;
892              
893             delete @{ $self->pnotes->{a2c}{session}{a2c}{openid} }{qw(
894             logged_in
895             last_accessed_time
896             openid_url
897             )};
898              
899             return Apache2::Const::OK;
900             }
901              
902             =head1 SEE ALSO
903              
904             L
905              
906             L
907              
908             L
909              
910             L
911              
912             L
913              
914             =head1 AUTHOR
915              
916             Mark Hedges, C<< >>
917              
918             =head1 COPYRIGHT & LICENSE
919              
920             Copyright 2008-2010 Mark Hedges, all rights reserved.
921              
922             This program is free software; you can redistribute it and/or modify it
923             under the same terms as Perl itself.
924              
925             =cut
926              
927             1; # End of Apache2::Controller::Auth::OpenID