File Coverage

blib/lib/Catalyst/Plugin/RapidApp/AuthCore/Controller/Auth.pm
Criterion Covered Total %
statement 97 149 65.1
branch 14 46 30.4
condition 17 51 33.3
subroutine 22 27 81.4
pod 0 11 0.0
total 150 284 52.8


line stmt bran cond sub pod time code
1             package Catalyst::Plugin::RapidApp::AuthCore::Controller::Auth;
2 1     1   1114 use Moose;
  1         9  
  1         28  
3 1     1   9034 use namespace::autoclean;
  1         13  
  1         33  
4              
5 1     1   161 BEGIN { extends 'Catalyst::Controller' };
6              
7 1     1   6055 use RapidApp::Util qw(:all);
  1         6  
  1         1200  
8             require Module::Runtime;
9             require Catalyst::Utils;
10 1     1   12 use URI;
  1         8  
  1         50  
11 1     1   10 use URI::Escape qw/uri_escape uri_unescape/;
  1         6  
  1         129  
12              
13 1     1 0 6 sub base :Chained :PathPrefix :CaptureArgs(0) {}
  1     8   1  
  1         34  
14              
15             # 'login' is the POST target of the new HTML login form:
16             sub login :Chained('base') :Args(0) {
17 5     5 0 2160 my $self = shift;
18 5         14 my $c = shift;
19            
20             # NEW: allow this action to be dual-use to display the login page
21             # for GET requests, and handle the login for POST requests
22 5 50       21 return $self->render_login_page($c) if ($c->req->method eq 'GET');
23              
24            
25             # if the user is logging back in, we keep their old session and just renew the user.
26             # if a new user is logging in, we throw away any existing session variables
27 5 50 66     314 my $haveSessionForUser = ($c->session_is_valid && $c->session->{RapidApp_username}) ? 1 : 0;
28            
29             # If a username has been posted, we force a re-login, even if we already have a session:
30 5 50       6099 $haveSessionForUser = 0 if ($c->req->params->{'username'});
31            
32 5 50       582 $self->handle_fresh_login($c) unless ($haveSessionForUser);
33            
34 5         64 return $self->do_redirect($c);
35 1     1   1622 }
  1         3  
  1         15  
36              
37              
38             sub _parse_to_referer {
39 0     0   0 my $self = shift;
40 0 0       0 my $c = shift or return undef;
41            
42 0         0 my $to = undef;
43            
44 0 0       0 if(my $referer = $c->req->referer) {
45 0         0 my $uri = $c->req->uri;
46 0         0 my $ruri = URI->new( $referer );
47             # Only consider local referer
48 0 0       0 if($uri->host_port eq $ruri->host_port) {
49             # encode/pass along the special query-string '_fragment'
50 0 0       0 if(my $fragment = $c->req->params->{_fragment}) {
51 0         0 $ruri->query_form( $ruri->query_form, _fragment => $fragment );
52             }
53 0         0 $to = $ruri->path_query;
54             }
55             }
56              
57 0         0 return $to;
58             }
59              
60             # Do a redirect back to the referer via login. Utilizes the new '?to=' feature
61             sub to_referer :Chained('base') :Args(0) {
62 0     0 0 0 my $self = shift;
63 0         0 my $c = shift;
64            
65 0         0 my $url = join($c->mount_url,'/auth/login');
66 0         0 my $to = $self->_parse_to_referer($c);
67            
68 0 0 0     0 if($to && $to ne '/' && $to ne $c->mount_url.'/') {
      0        
69 0 0 0     0 if ($c->session && $c->session_is_valid and $c->user_exists) {
      0        
70             # if we're already logged in, send them directly:
71 0         0 $url = $self->_url_extract_convert_fragment($to);
72             }
73             else {
74 0         0 $url = join('',$url,'?to=',uri_escape($to));
75             }
76             }
77            
78 0         0 $c->response->redirect($url, 307);
79 0         0 return $c->detach;
80 1     1   1176 }
  1         4  
  1         5  
81              
82             sub logout_to_referer :Chained('base') :Args(0) {
83 0     0 0 0 my $self = shift;
84 0         0 my $c = shift;
85            
86 0         0 my $url = '/';
87            
88 0 0       0 if(my $to = $self->_parse_to_referer($c)) {
89 0         0 $url = $self->_url_extract_convert_fragment($to);
90             }
91            
92 0         0 $c->delete_session('logout');
93 0         0 $c->logout;
94 0         0 $c->delete_expired_sessions;
95            
96 0         0 $c->response->redirect($url, 307);
97 0         0 return $c->detach;
98 1     1   1059 }
  1         8  
  1         10  
99              
100             sub logout :Chained('base') :Args(0) {
101 3     3 0 1441 my $self = shift;
102 3         12 my $c = shift;
103            
104 3         39 $c->delete_session('logout');
105 3         77451 $c->logout;
106 3         6430 $c->delete_expired_sessions;
107            
108 3         21378 return $self->do_redirect($c);
109 1     1   910 }
  1         2  
  1         15  
110              
111             sub do_redirect {
112 8     8 0 200 my ($self, $c, $href) = @_;
113 8   33     44 $c ||= RapidApp->active_request_context;
114 8   50     81 $href ||= $c->req->params->{redirect} || '/';
      33        
115            
116 8   50     1030 my $pfx = $c->mount_url || '';
117 8         72 $href =~ s/^${pfx}//;
118              
119             # If the client is still trying to redirect to '/auth/login' after login,
120             # it probably means they haven't configured any custom login redirect rules,
121             # send them to the best default location, the root module:
122 8 0 33     50 if($href =~ /^\/auth\/login\// && $c->session->{RapidApp_username}) {
123 0         0 my $new = join('/','',$c->module_root_namespace,'');
124 0         0 $new =~ s/\/+/\//g; #<-- strip double //
125             # Automatically swap '/auth/login/' for '/' (or where the root module is)
126 0         0 $href =~ s/^\/auth\/login\//${new}/;
127             }
128              
129 8 50       51 $href = "/$href" unless ($href =~ /^\//); #<-- enforce local
130 8         212 $c->response->redirect("$pfx$href");
131 8         1863 return $c->detach;
132             }
133              
134             # For session timeout, re-auth by RapidApp JavaScript client:
135             sub reauth :Chained('base') :Args(0) {
136 0     0 0 0 my $self = shift;
137 0         0 my $c = shift;
138            
139 0         0 my ($user, $pass)= ($c->req->params->{'username'}, $c->req->params->{'password'});
140            
141 0         0 $c->stash->{current_view} = 'RapidApp::JSON';
142 0 0       0 $c->stash->{json} = $self->do_login($c,$user,$pass) ?
143             { success => 1, msg => $user . ' logged in.' } :
144             { success => 0, msg => 'Logon failure.' };
145 1     1   1234 }
  1         7  
  1         12  
146              
147             # To be called within any controller to auth if needed
148             sub auth_verify :Private {
149 13     13 0 1177 my $self = shift;
150 13         46 my $c = shift;
151            
152 13         80 $c->delete_expired_sessions;
153            
154 13 100 66     53768 if ($c->session && $c->session_is_valid and $c->user_exists) {
      100        
155 8         24297 $c->res->header('X-RapidApp-Authenticated' => $c->user->username);
156             }
157             else {
158 5         11373 $c->res->header('X-RapidApp-Authenticated' => 0);
159 5 100       2295 if ( $c->is_ra_ajax_req ) {
160             # The false X-RapidApp-Authenticated header will automatically trigger
161             # reauth prompt by the JS client, if available
162 4         22 $c->res->header( 'Content-Type' => 'text/plain' );
163 4         1253 $c->res->body('not authenticated');
164 4         329 return $c->detach;
165             }
166             else {
167             # If this is a normal browser request (not Ajax) return the login page:
168 1         125 return $self->render_login_page($c);
169             }
170             }
171 1     1   1036 };
  1         6  
  1         15  
172              
173             sub _url_extract_convert_fragment {
174 0     0   0 my ($self, $url) = @_;
175              
176 0         0 my $uri = URI->new( $url );
177 0         0 my %qs = $uri->query_form;
178 0 0       0 if(my $fragment = $qs{_fragment}) {
179 0         0 delete $qs{_fragment};
180 0         0 $uri->query_form( \%qs );
181 0         0 $url = join('#',$uri->path_query,$fragment);
182             }
183 0         0 return $url
184             }
185              
186             sub do_login {
187 5     5 0 15 my $self = shift;
188 5         14 my $c = shift;
189 5         14 my $user = shift;
190 5         13 my $pass = shift;
191            
192 5         37 $c->_authcore_apply_login($user,$pass)
193             }
194              
195              
196             sub handle_fresh_login {
197 5     5 0 19 my $self = shift;
198 5         15 my $c = shift;
199            
200 5         24 my ($user, $pass)= ($c->req->params->{'username'}, $c->req->params->{'password'});
201            
202             # Don't try to login if there is no username supplied:
203 5 50 33     764 return unless ($user and $user ne '');
204            
205 5     5   53 try{$c->logout};
  5         183  
206            
207 5 50       7954 return if ($self->do_login($c,$user,$pass));
208            
209 0   0     0 $c->session->{login_error} ||= 'Authentication failure';
210            
211             # Something is broken!
212 0         0 $c->_save_session_expires;
213            
214             # Honor/persist the client's redirect if set and not '/'. Otherwise,
215             # redirect them back to the default login page. The theory is that
216             # their redirect target should be the same thing which displayed the
217             # login form to begin with. We want to redirect them so they are still
218             # on the login form to see the login error and be able to try again, but
219             # also maintain their redirect target across multiple failed login attempts.
220             # We don't assume this is the case by default for the root '/', but
221             # we don't need to worry about preserving the client's redirect target
222             # to '/' because it is already the default redirect target after login.
223 0         0 my $t = $c->req->params->{redirect};
224 0 0 0     0 my $redirect = ($t && $t ne '/' && $t ne '') ? $t : '/auth/login';
225 0         0 return $self->do_redirect($c,$redirect);
226             }
227              
228              
229              
230              
231             #sub valid_username {
232             # my $self = shift;
233             # my $username = shift;
234             # my $c = $self->c;
235             #
236             # my $User = $self->c->model('DB::User')->search_rs({
237             # 'me.username' => $username
238             # })->first or return 0;
239             #
240             # my $reason;
241             # my $ret = $User->can_login(\$reason);
242             # $c->session->{login_error} = $reason if (!$ret && $reason);
243             # return $ret;
244             #}
245              
246              
247             sub render_login_page {
248 1     1 0 3 my $self = shift;
249 1 50       6 my $c = shift or die '$c object arg missing!';
250 1   50     8 my $cnf = shift || {};
251            
252 1   50     6 my $config = $c->config->{'Plugin::RapidApp::AuthCore'} || {};
253 1   50     115 $config->{login_template} ||= 'rapidapp/public/login.html';
254            
255 1         14 my $ver_string = ref $c;
256 1         82 my $ver = eval('$' . $ver_string . '::VERSION');
257 1 50       9 $ver_string .= ' v' . $ver if ($ver);
258            
259             $cnf->{error_status} = delete $c->session->{login_error}
260 1 50 33     6 if($c->session && $c->session->{login_error});
261            
262             # New: preliminary rendering through the new Template::Controller:
263 1         287 my $TC = $c->template_controller;
264             my $body = $TC->template_render($config->{login_template},{
265             login_logo_url => $config->{login_logo_url}, #<-- default undef
266 1         128 form_post_url => join('/','',$self->action_namespace($c),'login'),
267             ver_string => $ver_string,
268             title => $ver_string . ' - Login',
269             %$cnf
270             },$c);
271            
272 1         60 $c->response->content_type('text/html; charset=utf-8');
273 1         280 $c->response->status(200);
274 1         136 $c->response->body($body);
275 1         51 return $c->detach;
276            
277             #%{$c->stash} = (
278             # %{$c->stash},
279             # template => $config->{login_template},
280             # login_logo_url => $config->{login_logo_url}, #<-- default undef
281             # form_post_url => '/auth/login',
282             # ver_string => $ver_string,
283             # title => $ver_string . ' - Login',
284             # %$cnf
285             #);
286             #
287             #return $c->detach( $c->view('RapidApp::TT') );
288             }
289             #######################################
290             #######################################
291              
292              
293             1;
294              
295             =head1 NAME
296              
297             Catalyst::Plugin::RapidApp::AuthCore::Controller::Auth - AuthCore Authentication Controller
298              
299             =head1 DESCRIPTION
300              
301             This is the controller which is automatically injected at C</auth> by the
302             L<AuthCore|Catalyst::Plugin::RapidApp::AuthCore> plugin and should not be called directly.
303              
304             See the L<AuthCore|Catalyst::Plugin::RapidApp::AuthCore> plugin documentation for more info.
305              
306             =head1 SEE ALSO
307              
308             =over
309              
310             =item *
311              
312             L<Catalyst::Plugin::RapidApp::AuthCore>
313              
314             =item *
315              
316             L<RapidApp::Manual>
317              
318             =back
319              
320             =head1 AUTHOR
321              
322             Henry Van Styn <vanstyn@cpan.org>
323              
324             =head1 COPYRIGHT AND LICENSE
325              
326             This software is copyright (c) 2013 by IntelliTree Solutions llc.
327              
328             This is free software; you can redistribute it and/or modify it under
329             the same terms as the Perl 5 programming language system itself.
330              
331             =cut
332              
333