File Coverage

blib/lib/Catalyst/Plugin/RapidApp/AuthCore/Controller/Auth.pm
Criterion Covered Total %
statement 106 162 65.4
branch 17 54 31.4
condition 18 54 33.3
subroutine 22 27 81.4
pod 0 11 0.0
total 163 308 52.9


line stmt bran cond sub pod time code
1             package Catalyst::Plugin::RapidApp::AuthCore::Controller::Auth;
2 1     1   1121 use Moose;
  1         10  
  1         22  
3 1     1   8950 use namespace::autoclean;
  1         12  
  1         29  
4              
5 1     1   141 BEGIN { extends 'Catalyst::Controller' };
6              
7 1     1   5952 use RapidApp::Util qw(:all);
  1         2  
  1         1226  
8             require Module::Runtime;
9             require Catalyst::Utils;
10 1     1   12 use URI;
  1         5  
  1         49  
11 1     1   7 use URI::Escape qw/uri_escape uri_unescape/;
  1         9  
  1         124  
12              
13 1     1 0 16 sub base :Chained :PathPrefix :CaptureArgs(0) {}
  1     8   3  
  1         32  
14              
15             # 'login' is the POST target of the new HTML login form:
16             sub login :Chained('base') :Args(0) {
17 5     5 0 1799 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       17 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     263 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       4895 $haveSessionForUser = 0 if ($c->req->params->{'username'});
31            
32 5 50       459 $self->handle_fresh_login($c) unless ($haveSessionForUser);
33            
34 5         79 return $self->do_redirect($c);
35 1     1   1608 }
  1         12  
  1         10  
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   1202 }
  1         7  
  1         10  
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   976 }
  1         7  
  1         10  
99              
100             sub logout :Chained('base') :Args(0) {
101 3     3 0 1128 my $self = shift;
102 3         8 my $c = shift;
103            
104 3         26 $c->delete_session('logout');
105 3         56214 $c->logout;
106 3         4973 $c->delete_expired_sessions;
107            
108 3         16183 return $self->do_redirect($c);
109 1     1   956 }
  1         6  
  1         17  
110              
111             sub do_redirect {
112 8     8 0 183 my ($self, $c, $href) = @_;
113 8   33     35 $c ||= RapidApp->active_request_context;
114 8   50     50 $href ||= $c->req->params->{redirect} || '/';
      33        
115            
116 8   50     668 my $pfx = $c->mount_url || '';
117 8         69 $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     30 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       39 $href = "/$href" unless ($href =~ /^\//); #<-- enforce local
130 8         175 $c->response->redirect("$pfx$href");
131 8         1397 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   1220 }
  1         6  
  1         16  
146              
147             # To be called within any controller to auth if needed
148             sub auth_verify :Private {
149 13     13 0 955 my $self = shift;
150 13         39 my $c = shift;
151            
152 13         62 $c->delete_expired_sessions;
153            
154 13 100 66     42432 if ($c->session && $c->session_is_valid and $c->user_exists) {
      100        
155 8         18056 $c->res->header('X-RapidApp-Authenticated' => $c->user->username);
156             }
157             else {
158 5         8662 $c->res->header('X-RapidApp-Authenticated' => 0);
159 5 100       1778 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         19 $c->res->header( 'Content-Type' => 'text/plain' );
163 4         860 $c->res->body('not authenticated');
164 4         231 return $c->detach;
165             }
166             else {
167             # If this is a normal browser request (not Ajax) return the login page:
168 1         93 return $self->render_login_page($c);
169             }
170             }
171 1     1   1069 };
  1         4  
  1         20  
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 14 my $self = shift;
188 5         11 my $c = shift;
189 5         12 my $user = shift;
190 5         11 my $pass = shift;
191            
192 5         38 $c->delete_expired_sessions;
193            
194 5 50       13027 if($c->authenticate({ username => $user, password => $pass })) {
195 5         1115 $c->session->{RapidApp_username} = $user;
196            
197             # New: set the X-RapidApp-Authenticated header now so the response
198             # itself will reflect the successful login (since in either case, the
199             # immediate response is a simple redirect). This is for client info/debug only
200 5         566 $c->res->header('X-RapidApp-Authenticated' => $c->user->username);
201              
202 5 50       1618 $c->log->info("Successfully authenticated user '$user'") if ($c->debug);
203 5         52 $c->user->update({
204             last_login_ts => DateTime->now( time_zone => 'local' )
205             });
206            
207             # Something is broken!
208 5         49 $c->_save_session_expires;
209            
210             # New: upon successful login, override redirect target with client-supplied 'to' if present.
211             # This provides a mechanism for the client to set its own redirect target via ?to=/some/path
212 5         4295 my $to = $c->req->params->{to};
213 5 50 33     440 if ($to && $to ne '') {
214 0         0 $to = $self->_url_extract_convert_fragment( uri_unescape($to) );
215 0         0 $c->req->params->{redirect} = $to;
216             }
217            
218 5         33 return 1;
219             }
220             else {
221 0 0       0 $c->log->info("Authentication failed for user '$user'") if ($c->debug);
222 0         0 return 0;
223             }
224             }
225              
226              
227             sub handle_fresh_login {
228 5     5 0 14 my $self = shift;
229 5         11 my $c = shift;
230            
231 5         18 my ($user, $pass)= ($c->req->params->{'username'}, $c->req->params->{'password'});
232            
233             # Don't try to login if there is no username supplied:
234 5 50 33     618 return unless ($user and $user ne '');
235            
236 5     5   43 try{$c->logout};
  5         147  
237            
238 5 50       6391 return if ($self->do_login($c,$user,$pass));
239            
240 0   0     0 $c->session->{login_error} ||= 'Authentication failure';
241            
242             # Something is broken!
243 0         0 $c->_save_session_expires;
244            
245             # Honor/persist the client's redirect if set and not '/'. Otherwise,
246             # redirect them back to the default login page. The theory is that
247             # their redirect target should be the same thing which displayed the
248             # login form to begin with. We want to redirect them so they are still
249             # on the login form to see the login error and be able to try again, but
250             # also maintain their redirect target across multiple failed login attempts.
251             # We don't assume this is the case by default for the root '/', but
252             # we don't need to worry about preserving the client's redirect target
253             # to '/' because it is already the default redirect target after login.
254 0         0 my $t = $c->req->params->{redirect};
255 0 0 0     0 my $redirect = ($t && $t ne '/' && $t ne '') ? $t : '/auth/login';
256 0         0 return $self->do_redirect($c,$redirect);
257             }
258              
259              
260              
261              
262             #sub valid_username {
263             # my $self = shift;
264             # my $username = shift;
265             # my $c = $self->c;
266             #
267             # my $User = $self->c->model('DB::User')->search_rs({
268             # 'me.username' => $username
269             # })->first or return 0;
270             #
271             # my $reason;
272             # my $ret = $User->can_login(\$reason);
273             # $c->session->{login_error} = $reason if (!$ret && $reason);
274             # return $ret;
275             #}
276              
277              
278             sub render_login_page {
279 1     1 0 3 my $self = shift;
280 1 50       3 my $c = shift or die '$c object arg missing!';
281 1   50     7 my $cnf = shift || {};
282            
283 1   50     4 my $config = $c->config->{'Plugin::RapidApp::AuthCore'} || {};
284 1   50     86 $config->{login_template} ||= 'rapidapp/public/login.html';
285            
286 1         2 my $ver_string = ref $c;
287 1         53 my $ver = eval('$' . $ver_string . '::VERSION');
288 1 50       7 $ver_string .= ' v' . $ver if ($ver);
289            
290             $cnf->{error_status} = delete $c->session->{login_error}
291 1 50 33     5 if($c->session && $c->session->{login_error});
292            
293             # New: preliminary rendering through the new Template::Controller:
294 1         222 my $TC = $c->template_controller;
295             my $body = $TC->template_render($config->{login_template},{
296             login_logo_url => $config->{login_logo_url}, #<-- default undef
297 1         91 form_post_url => join('/','',$self->action_namespace($c),'login'),
298             ver_string => $ver_string,
299             title => $ver_string . ' - Login',
300             %$cnf
301             },$c);
302            
303 1         51 $c->response->content_type('text/html; charset=utf-8');
304 1         223 $c->response->status(200);
305 1         112 $c->response->body($body);
306 1         41 return $c->detach;
307            
308             #%{$c->stash} = (
309             # %{$c->stash},
310             # template => $config->{login_template},
311             # login_logo_url => $config->{login_logo_url}, #<-- default undef
312             # form_post_url => '/auth/login',
313             # ver_string => $ver_string,
314             # title => $ver_string . ' - Login',
315             # %$cnf
316             #);
317             #
318             #return $c->detach( $c->view('RapidApp::TT') );
319             }
320             #######################################
321             #######################################
322              
323              
324             1;
325              
326             =head1 NAME
327              
328             Catalyst::Plugin::RapidApp::AuthCore::Controller::Auth - AuthCore Authentication Controller
329              
330             =head1 DESCRIPTION
331              
332             This is the controller which is automatically injected at C</auth> by the
333             L<AuthCore|Catalyst::Plugin::RapidApp::AuthCore> plugin and should not be called directly.
334              
335             See the L<AuthCore|Catalyst::Plugin::RapidApp::AuthCore> plugin documentation for more info.
336              
337             =head1 SEE ALSO
338              
339             =over
340              
341             =item *
342              
343             L<Catalyst::Plugin::RapidApp::AuthCore>
344              
345             =item *
346              
347             L<RapidApp::Manual>
348              
349             =back
350              
351             =head1 AUTHOR
352              
353             Henry Van Styn <vanstyn@cpan.org>
354              
355             =head1 COPYRIGHT AND LICENSE
356              
357             This software is copyright (c) 2013 by IntelliTree Solutions llc.
358              
359             This is free software; you can redistribute it and/or modify it under
360             the same terms as the Perl 5 programming language system itself.
361              
362             =cut
363              
364